Como lidar com cliques de botão usando o XML onClick em Fragments

440

Antes do favo de mel (Android 3), cada atividade foi registrada para manipular cliques de botão por meio da onClicktag no XML de um layout:

android:onClick="myClickMethod"

Dentro desse método, você pode usar view.getId()e uma instrução switch para fazer a lógica do botão.

Com a introdução do Honeycomb, estou dividindo essas atividades em fragmentos que podem ser reutilizados em muitas atividades diferentes. A maior parte do comportamento dos botões é independente da atividade, e eu gostaria que o código residisse no arquivo Fragments sem usar o método antigo (anterior à 1.6) de registrar o OnClickListenerbotão para cada botão.

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

O problema é que, quando meu layout é inflado, ainda é a Atividade de hospedagem que está recebendo o botão, não os Fragmentos individuais. Existe uma boa abordagem para

  • Registre o fragmento para receber os cliques do botão?
  • Passar os eventos de clique da Atividade para o fragmento ao qual eles pertencem?
smith324
fonte
1
Você não consegue lidar com o registro de ouvintes no onCreate do fragmento?
CL22
24
@jodes Sim, mas não quero usar setOnClickListenere, findViewByIdpara cada botão, onClickfoi por isso que foi adicionado, para simplificar as coisas.
22611 smith324
4
Olhando para a resposta aceita, acho que o uso do setOnClickListener é mais frouxo do que aderir à abordagem XML onClick. Se a atividade precisar 'encaminhar' cada clique para o fragmento direito, isso significa que o código precisará ser alterado sempre que um fragmento for adicionado. Usar uma interface para se separar da classe base do fragmento não ajuda nisso. Se o fragmento se registrar com o próprio botão correto, a atividade permanecerá completamente independente do que é melhor estilo IMO. Veja também a resposta de Adorjan Princz.
Adriaan Koster
@ smith324 tem que concordar com Adriaan neste. Dê uma olhada na resposta de Adorjan e veja se a vida não é melhor depois disso.
user1567453

Respostas:

175

Você poderia fazer isso:

Atividade:

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

Em resposta a @Ameen, que queria menos acoplamento, os fragmentos são reutilizáveis

Interface:

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

Atividade:

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
    }
}    
Blundell
fonte
52
É o que estou fazendo agora, essencialmente, mas é muito mais confuso quando você tem vários fragmentos que precisam receber eventos de clique. Estou apenas irritado com fragmentos em geral porque os paradigmas se dissolveram em torno deles.
precisa saber é
128
Estou enfrentando o mesmo problema e, apesar de agradecer sua resposta, esse não é um código limpo do ponto de vista da engenharia de software. Esse código resulta na atividade sendo fortemente acoplada ao fragmento. Você deve poder reutilizar o mesmo fragmento em várias atividades sem que as atividades conheçam os detalhes de implementação dos fragmentos.
Amin
1
Deve ser "switch (v.getId ()) {" e não "switch (v.getid ()) {"
eb80
5
Em vez de definir sua própria interface, você pode usar o OnClickListener já existente, conforme mencionado pelo Euporie.
Fr00tyl00p
1
Eu chorei quando li isso, é MUITO BOILERPLATE ... A resposta abaixo de @AdorjanPrincz é o caminho a percorrer.
precisa saber é o seguinte
603

Prefiro usar a seguinte solução para manipular eventos onClick. Isso funciona para Atividade e Fragmentos também.

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
fonte
9
No onCreateView, eu percorro todos os itens filhos do ViewGroup v e defino o onclicklistener para todas as instâncias de Button que encontro. É muito melhor do que definir manualmente o ouvinte para todos os botões.
Uma Pessoa
17
Votado. Isso torna os fragmentos reutilizáveis. Caso contrário, por que usar fragmentos?
boreas
44
Não é a mesma técnica preconizada pela programação do Windows em 1987? Não se preocupe. O Google se move rapidamente e tem tudo a ver com a produtividade do desenvolvedor. Tenho certeza que não demorará muito até que o tratamento de eventos seja tão bom quanto o Visual Basic de 1991-eara.
Edward Brey
7
Que importação de bruxa você usou para OnClickListener? Intellij sugere me android.view.View.OnClickListener e ele não funciona: / (onClick nunca se)
Lucas Jota
4
@ NathanOsman, penso que a questão estava relacionada com o xml onClick, pelo que os ans aceitos fornecem a solução exata.
Naveed Ahmad #
29

O problema que penso é que a visão ainda é a atividade, não o fragmento. Os fragmentos não têm nenhuma visão independente própria e estão anexados à visão de atividades pai. É por isso que o evento termina na Atividade, não no fragmento. É lamentável, mas acho que você precisará de algum código para fazer isso funcionar.

O que tenho feito durante as conversões é simplesmente adicionar um ouvinte de clique que chama o manipulador de eventos antigo.

por exemplo:

final Button loginButton = (Button) view.findViewById(R.id.loginButton);
loginButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(final View v) {
        onLoginClicked(v);
    }
});
Brill Pappin
fonte
1
Obrigado - usei isso com uma pequena modificação, pois estou passando a visualização do fragmento (isto é, o resultado de inflater.inflate (R.layout.my_fragment_xml_resource)) para onLoginClicked () para que ele possa acessar as sub-visualizações dos fragmentos, como um EditText, via view.findViewById () (se eu simplesmente passar pela exibição de atividade, as chamadas para view.findViewById (R.id.myfragmentwidget_id) retornarão nulo).
Michael Nelson
Isso não funciona com a API 21 no meu projeto. Alguma idéia de como usar essa abordagem?
Darth Coder
Seu código bastante básico, usado em praticamente todos os aplicativos. Você pode descrever o que está acontecendo para você?
Brill Pappin
1
Faça o meu voto positivo a esta resposta para obter uma explicação do problema que ocorreu porque o layout do fragmento está anexado à visualização da atividade.
fllo
25

Recentemente, resolvi esse problema sem precisar adicionar um método ao contexto Activity ou ter que implementar o OnClickListener. Não tenho certeza se é uma solução "válida" também, mas funciona.

Baseado em: https://developer.android.com/tools/data-binding/guide.html#binding_events

Isso pode ser feito com ligações de dados: basta adicionar sua instância de fragmento como uma variável e vincular qualquer método ao 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 o código de ligação do fragmento seria ...

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
fonte
1
Eu só tenho um sentimento, alguns detalhes estão faltando desde que eu não sou capaz de conseguir esta solução trabalho
Tima
Talvez esteja faltando alguma coisa no "Ambiente de Compilação" na documentação: developer.android.com/tools/data-binding/…
Aldo Canepa
@ Aldo Para o método onClick em XML, acredito que você deva ter o android: onClick = "@ {() -> fragmento. ButtonClicked ()}". Também para outros, você deve declarar a função buttonClicked () dentro do fragmento e colocar sua lógica dentro.
Hayk Nahapetyan
no XML que deve serandroid:name="fragment" android:type="com.example.testapp.fragments.CustomFragment"/>
COYG
1
Eu apenas tentei isso e os atributos namee typeda variabletag não devem ter o android:prefixo. Talvez exista uma versão antiga dos layouts do Android que exija isso?
JohnnyLambada
6

ButterKnife é provavelmente a melhor solução para o problema da desordem. Ele usa processadores de anotação para gerar o chamado código do "método antigo".

Mas o método onClick ainda pode ser usado, com um inflador personalizado.

Como usar

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

Implementação

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
fonte
6

Eu prefiro usar o tratamento de cliques no código do que usar o onClickatributo em XML ao trabalhar com fragmentos.

Isso fica ainda mais fácil ao migrar suas atividades para fragmentos. Você pode simplesmente chamar o manipulador de cliques (definido anteriormente android:onClickem XML) diretamente de cada casebloco.

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 se trata de lidar com cliques em fragmentos, isso me parece mais simples do que android:onClick.

Ronnie
fonte
5

Esta é outra maneira:

1.Crie um BaseFragment como este:

public abstract class BaseFragment extends Fragment implements OnClickListener

2.Use

public class FragmentA extends BaseFragment 

ao invés de

public class FragmentA extends Fragment

3.Em sua atividade:

public class MainActivity extends ActionBarActivity implements OnClickListener

e

BaseFragment fragment = new FragmentA;

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

Espero que ajude.

Euporie
fonte
1 ano, 1 mês e 1 dia após a sua resposta: Existe algum motivo além de não repetir a implementação do OnClickListener em cada classe Fragment para criar o BaseFragment abstrato?
Dimitrios K.
5

No meu caso de uso, tenho 50 ImageViews ímpares que eu precisava conectar em um único método onClick. Minha solução é fazer um loop sobre as visualizações dentro do fragmento e definir o mesmo ouvinte onclick em cada:

    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
fonte
2

A meu ver, as respostas são antigas. Recentemente, o Google introduziu o DataBinding, que é muito mais fácil de manipular ao clicar ou atribuir no seu xml.

Aqui está um bom exemplo, que você pode ver como lidar com isso:

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

Também há um tutorial muito bom sobre o DataBinding, que você pode encontrar aqui .

Amir
fonte
2

Você pode definir um retorno de chamada como um atributo do seu layout XML. O artigo Atributos XML personalizados para seus widgets personalizados do Android mostrará como fazer isso para um widget personalizado. O crédito vai para Kevin Dion :)

Estou investigando se posso adicionar atributos estilizados à classe Fragment base.

A idéia básica é ter a mesma funcionalidade que o View implementa ao lidar com o retorno de chamada onClick.

Nelson Ramirez
fonte
1

Adicionando à resposta de Blundell,
se você tiver mais fragmentos, com bastante onClicks:

Atividade:

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

} 

Em seu fragmento:

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

Se você se registrar no xml usando android: Onclick = "", o retorno de chamada será dado à Atividade respeitada em cujo contexto seu fragmento pertence (getActivity ()). Se esse método não for encontrado na Atividade, o sistema lançará uma exceção.

Isham
fonte
obrigado, ninguém mais explicou por que a falha estava ocorrendo #
1

Você pode considerar usar o EventBus para eventos dissociados. Você pode ouvir eventos com muita facilidade. Você também pode garantir que o evento esteja sendo recebido no thread da interface do usuário (em vez de chamar runOnUiThread .. para si mesmo para cada assinatura de evento)

https://github.com/greenrobot/EventBus

do Github:

Barramento de eventos otimizado para Android que simplifica a comunicação entre Atividades, Fragmentos, Threads, Serviços, etc. Menos código, melhor qualidade

programador
fonte
1

Eu gostaria de acrescentar à resposta de Adjorn Linkz .

Se você precisar de vários manipuladores, poderá usar referências lambda

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

O truque aqui é que handlera assinatura do método corresponde à View.OnClickListener.onClickassinatura. Dessa forma, você não precisará da View.OnClickListenerinterface.

Além disso, você não precisará de nenhuma declaração de chave.

Infelizmente, esse método é limitado apenas a interfaces que requerem um único método ou uma lambda.

Dragas
fonte
1

Embora eu tenha encontrado algumas respostas legais baseadas na ligação de dados, não vi nada disso com essa abordagem - no sentido de permitir a resolução de fragmentos e permitir definições de layout sem fragmentos nos XMLs.

Supondo que a ligação de dados esteja ativada, aqui está uma solução genérica que posso propor; Um pouco longo, mas definitivamente funciona (com algumas ressalvas):

Etapa 1: implementação personalizada do OnClick

Isso executará uma pesquisa com reconhecimento de fragmentos nos contextos associados à visualização tocada (por exemplo, botão):


// 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: vagamente baseado na implementação original do Android (consulte https://android.googlesource.com/platform/frameworks/base/+/a175a5b/core/java/android/view/View.java#3025 )

Etapa 2: aplicativo declarativo em arquivos de layout

Em seguida, nos XMLs com reconhecimento de ligação de dados:

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

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

Ressalvas

  • Assume uma FragmentActivityimplementação baseada em 'moderna'
  • Só é possível pesquisar o método do fragmento "mais alto" (ou seja, o último ) na pilha (embora isso possa ser corrigido, se necessário)
d4vidi
fonte
0

Isso tem funcionado para mim: (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
Vinod Joshi
fonte
1
Isso duplica a resposta do @Brill Pappin .
NaXa
0

Melhor solução IMHO:

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

em seguida, no onViewStateRestored do Fragment:

addClick(R.id.myButton);
user4806368
fonte
0

Sua atividade está recebendo o retorno de chamada como deve ter sido usado:

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

Se você deseja que seu fragmento receba retorno de chamada, faça o seguinte:

mViewPagerCloth.setOnClickListener(this);

e implementar onClickListenerinterface no Fragment

Fusão a frio
fonte