Как обрабатывать нажатия кнопок, используя XML onClick внутри фрагментов


440

Перед сотой (Android 3) каждая активность была зарегистрирована для обработки нажатий кнопок через onClickтег в XML макета:

android:onClick="myClickMethod"

В этом методе вы можете использовать view.getId()и оператор switch для логики кнопок.

С введением Honeycomb я разбил эти действия на фрагменты, которые можно повторно использовать во многих различных видах деятельности. Большая часть поведения кнопок не зависит от активности, и я хотел бы, чтобы код находился внутри файла фрагментов без использования старого (до 1.6) метода регистрации OnClickListenerкаждой кнопки.

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

Проблема в том, что, когда мои макеты завышены, все же хост-активность получает нажатия кнопок, а не отдельные фрагменты. Есть ли хороший подход к

  • Зарегистрировать фрагмент, чтобы получать нажатия кнопок?
  • Передать события щелчка от Activity к фрагменту, которому они принадлежат?

1
Не можете ли вы обработать регистрацию слушателей в onCreate фрагмента?
CL22 22

24
@jodes Да, но я не хочу использовать setOnClickListenerи findViewByIdдля каждой кнопки, поэтому onClickбыл добавлен, чтобы сделать вещи проще.
smith324

4
Глядя на принятый ответ, я думаю, что использование setOnClickListener более слабо связано, чем использование подхода XML onClick. Если действие должно «пересылать» каждый клик на нужный фрагмент, это означает, что код должен будет меняться при каждом добавлении фрагмента. Использование интерфейса для отделения от базового класса фрагмента не помогает в этом. Если фрагмент регистрируется с правильной самой кнопкой, активность остается полностью агностичной, что является лучшим стилем IMO. Смотрите также ответ от Adorjan Princz.
Адриан Костер

@ smith324 должен согласиться с Адрианом на этот счет. Попробуй ответ Адоржана и посмотри, не станет ли жизнь после этого лучше.
user1567453

Ответы:


175

Вы можете просто сделать это:

Деятельность:

Fragment someFragment;    

//...onCreate etc instantiating your fragments

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

Фрагмент:

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

В ответ на @ Ameen, который хотел меньше связи, поэтому фрагменты можно использовать повторно

Интерфейс:

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

Деятельность:

XmlClickable someFragment;    

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

Фрагмент:

public class SomeFragment implements XmlClickable {

//...onCreateView, etc.

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

52
Это то, что я делаю сейчас, но гораздо сложнее, когда у вас есть несколько фрагментов, каждый из которых должен получать события щелчка. Я просто раздражен фрагментами в целом, потому что парадигмы растворились вокруг них.
smith324

128
Я сталкиваюсь с той же проблемой, и хотя я ценю ваш ответ, это не чистый код с точки зрения разработки программного обеспечения. Этот код приводит к тому, что активность тесно связана с фрагментом. Вы должны иметь возможность повторно использовать один и тот же фрагмент в нескольких действиях, при этом действия не будут знать детали реализации фрагментов.
Амин

1
Должен быть "switch (v.getId ()) {", а не "switch (v.getid ()) {"
eb80

5
Вместо того, чтобы определять свой собственный интерфейс, вы можете использовать уже существующий OnClickListener, как упомянуто Euporie.
fr00tyl00p

1
Я плакал, когда я читал это, это НАМНОГО БОЛЬШОЙ КОПИИ ... Ответ ниже @AdorjanPrincz - путь.
Wjdavis5

603

Я предпочитаю использовать следующее решение для обработки событий onClick. Это работает для деятельности и фрагментов, а также.

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
В onCreateView я перебираю все дочерние элементы ViewGroup v и устанавливаю onclicklistener для всех найденных экземпляров Button. Это намного лучше, чем ручная настройка слушателя для всех кнопок.
Человек

17
Проголосовали. Это делает фрагменты многоразовыми. Иначе зачем использовать фрагменты?
Борей

44
Разве это не та же самая техника, отстаиваемая Программированием Windows еще в 1987 году? Не беспокоиться. Google движется быстро и полностью зависит от продуктивности разработчиков. Я уверен, что пройдет не так много времени, пока обработка событий не станет такой же эффективной, как в Visual Basic 1991 года.
Эдвард Брей

7
импорт ведьм вы использовали для OnClickListener? Intellij предлагает мне android.view.View.OnClickListener, и он не работает: / (onClick никогда не запускается)
Lucas Jota

4
@NathanOsman Я думаю, что Вопрос был связан с xml onClick, поэтому принятые ответы предоставляют точное решение.
Навид Ахмад

29

Проблема, я думаю, в том, что представление - это все еще деятельность, а не фрагмент. Фрагменты не имеют собственного независимого представления и привязаны к представлению родительских действий. Вот почему событие заканчивается в деятельности, а не фрагмент. Это прискорбно, но я думаю, что вам понадобится код для этой работы.

То, что я делал во время преобразований, это просто добавление прослушивателя щелчков, который вызывает старый обработчик событий.

например:

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

1
Спасибо - я использовал это с одной небольшой модификацией в том, что я передаю представление фрагмента (т.е. результат inflater.inflate (R.layout.my_fragment_xml_resource)) в onLoginClicked (), чтобы он мог получить доступ к вложенным представлениям фрагментов, такой как EditText, через view.findViewById () (если я просто прохожу через представление деятельности, вызовы view.findViewById (R.id.myfragmentwidget_id) возвращают ноль).
Майкл Нельсон

Это не работает с API 21 в моем проекте. Есть мысли о том, как использовать этот подход?
Дарт Кодер

Это довольно простой код, используемый почти во всех приложениях. Можете ли вы описать, что происходит для вас?
Брилл Паппин

1
Сделайте мой ответ на этот ответ для объяснения проблемы, которая произошла, потому что расположение фрагмента привязано к виду деятельности.
Фло

25

Недавно я решил эту проблему, не добавляя метод в контекстную активность или не реализуя OnClickListener. Я не уверен, что это «правильное» решение, но оно работает.

Основано на: https://developer.android.com/tools/data-binding/guide.html#binding_events

Это можно сделать с помощью привязок данных: просто добавьте свой фрагмент в качестве переменной, затем вы можете связать любой метод с помощью 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>

И фрагмент, связывающий код будет ...

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
У меня просто ощущение, что некоторые детали отсутствуют, так как я не могу заставить это решение работать
Тима

Может быть, вы что-то упустили из «Среды сборки» в документации: developer.android.com/tools/data-binding/…
Альдо Канепа

@Aldo Для метода onClick в XML я считаю, что у вас должен быть android: onClick = "@ {() -> фрагмент. ButtonClicked ()}". Также для других вы должны объявить функцию buttonClicked () внутри фрагмента и поместить свою логику внутрь.
Айк Нахапетян

в XML это должно бытьandroid:name="fragment" android:type="com.example.testapp.fragments.CustomFragment"/>
COYG

1
Я просто попытался это и nameи typeатрибуты variableтега должен не иметь android:префикс. Возможно, есть старая версия макетов Android, которая требует этого?
JohnnyLambada

6

ButterKnife , вероятно, является лучшим решением проблемы беспорядка. Он использует процессоры аннотаций для генерации так называемого стандартного кода «старого метода».

Но метод onClick все еще можно использовать с пользовательским инфлятором.

Как пользоваться

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

Реализация

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

Я предпочел бы перейти на обработку кликов в коде, чем на использование onClickатрибута в XML при работе с фрагментами.

Это становится еще проще при переносе ваших действий на фрагменты. Вы можете просто вызвать обработчик щелчка (ранее установленный android:onClickв XML) непосредственно из каждого caseблока.

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

Когда дело доходит до обработки кликов во фрагментах, это выглядит для меня проще, чем android:onClick.


5

Это еще один способ:

1. Создайте базовый фрагмент следующим образом:

public abstract class BaseFragment extends Fragment implements OnClickListener

2.Use

public class FragmentA extends BaseFragment 

вместо

public class FragmentA extends Fragment

3. В вашей деятельности:

public class MainActivity extends ActionBarActivity implements OnClickListener

а также

BaseFragment fragment = new FragmentA;

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

Надеюсь, поможет.


Через 1 год, 1 месяц и 1 день после вашего ответа. Есть ли какая-либо причина, кроме неповторения реализации OnClickListener для каждого класса Fragment, для создания абстрактного BaseFragment?
Димитриос К.

5

В моем случае у меня есть 50 нечетных ImageViews, которые мне нужно было подключить к одному методу onClick. Мое решение состоит в том, чтобы перебрать представления внутри фрагмента и установить одинаковый слушатель onclick для каждого:

    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

Как я вижу ответы, они как-то старые. Недавно Google представил DataBinding, который намного проще обрабатывать onClick или назначать в xml.

Вот хороший пример, который вы можете увидеть, как справиться с этим:

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

Существует также очень хороший учебник по связыванию данных, вы можете найти его здесь .


2

Вы можете определить обратный вызов как атрибут вашего макета XML. В статье Пользовательские атрибуты XML для ваших пользовательских виджетов Android будет показано, как это сделать для пользовательского виджета. Кредит идет к Кевину Диону :)

Я изучаю, могу ли я добавить настраиваемые атрибуты в базовый класс Fragment.

Основная идея заключается в том, чтобы иметь ту же функциональность, которую реализует View при работе с обратным вызовом onClick.


1

В дополнение к ответу Бланделла,
если у вас есть больше фрагментов, с большим количеством onClicks:

Деятельность:

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

} 

В вашем фрагменте:

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

1

Если вы зарегистрируетесь в xml с помощью android: Onclick = "", обратный вызов будет передан уважаемой активности, в контексте которой принадлежит ваш фрагмент (getActivity ()). Если такой метод не найден в Activity, то система выдаст исключение.


спасибо, никто не объяснил, почему произошел сбой

1

Возможно, вы захотите использовать EventBus для развязанных событий. Вы можете очень легко прослушивать события. Вы также можете убедиться, что событие принимается в потоке пользовательского интерфейса (вместо вызова runOnUiThread .. для себя для каждой подписки на событие)

https://github.com/greenrobot/EventBus

из Github:

Оптимизированная для Android шина событий, которая упрощает связь между операциями, фрагментами, потоками, службами и т. Д. Меньше кода, лучшее качество


1

Я хотел бы добавить к Adjorn Linkz в ответ .

Если вам нужно несколько обработчиков, вы можете просто использовать лямбда-ссылки

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

Хитрость в том, что handlerподпись этого метода совпадает с View.OnClickListener.onClickподписью. Таким образом, вам не понадобится View.OnClickListenerинтерфейс.

Кроме того, вам не понадобятся какие-либо операторы switch.

К сожалению, этот метод ограничен только интерфейсами, которые требуют одного метода или лямбда-выражения.


1

Несмотря на то, что я нашел несколько хороших ответов, основанных на связывании данных, я не видел, чтобы в полной мере использовался этот подход - в смысле включения разрешения фрагментов при одновременном определении без макетов определений макетов в XML.

Таким образом , предполагающие привязка данных будет включена, вот общее решение , которое я могу предложить; Немного долго, но это определенно работает (с некоторыми оговорками):

Шаг 1: Пользовательская реализация OnClick

Это запустит поиск с учетом фрагментов в контекстах, связанных с включенным представлением (например, кнопка):


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

Примечание. Основано на оригинальной реализации Android (см. Https://android.googlesource.com/platform/frameworks/base/+/a175a5b/core/java/android/view/View.java#3025 ).

Шаг 2: Декларативное приложение в файлах макета

Затем в привязке данных с поддержкой XML:

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

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

Предостережения

  • Предполагается «современная» FragmentActivityреализация
  • Может только искать метод «самого верхнего» (т.е. последнего ) фрагмента в стеке (хотя это может быть исправлено, если это необходимо)

0

Это работает для меня: (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
Это дублирует ответ @Brill Pappin .
NaXa

0

Лучшее решение ИМХО:

во фрагменте:

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

затем в onViewStateRestored фрагмента:

addClick(R.id.myButton);

0

Ваша активность получает обратный вызов, как должны были использовать:

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

Если вы хотите, чтобы ваш фрагмент получил обратный вызов, сделайте следующее:

mViewPagerCloth.setOnClickListener(this);

и реализовать onClickListenerинтерфейс на Fragment

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.