Як обробляти натискання кнопок за допомогою XML onClick у фрагментах


440

Попередньо стільник (Android 3), кожну активність було зареєстровано для обробки натискань кнопок за допомогою onClickтегу в XML-макеті:

android:onClick="myClickMethod"

У рамках цього методу ви можете використовувати view.getId()і оператор перемикання, щоб виконувати логіку кнопки.

З впровадженням соти я розбиваю ці дії на фрагменти, які можна повторно використовувати у багатьох різних видах діяльності. Більшість поведінки кнопок не залежать від активності, і я хотів би, щоб код знаходився у файлі Fragments без використання старого (попереднього 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
    }
});

Проблема полягає в тому, що коли моє розташування завищене, все одно активність хостингу отримує натискання кнопки, а не окремі фрагменти. Чи є хороший підхід до будь-якого

  • Зареєструвати фрагмент, щоб отримати натискання кнопки?
  • Передати події кліку з Активності до фрагменту, якому вони належать?

1
Ви не можете впоратися з реєстрацією слухачів у фрагменті onCreate?
CL22

24
@jodes Так, але я не хочу використовувати setOnClickListenerі findViewByIdдля кожної кнопки, тому onClickбуло додано, щоб зробити речі простішими.
smith324

4
Дивлячись на прийняту відповідь, я думаю, що використання setOnClickListener є більш вільно пов'язаним, ніж дотримуватися підходу XML onClick. Якщо діяльність повинна "пересувати" кожен клік до правого фрагмента, це означає, що код доведеться змінювати щоразу, коли фрагмент додається. Використання інтерфейсу для від'єднання від базового класу фрагмента не допомагає у цьому. Якщо фрагмент реєструється правильною кнопкою, активність залишається повністю агностичною, що є кращим стилем IMO. Дивіться також відповідь від Adorjan Princz.
Адріан Костер

@ smith324 повинні погодитись з Адріаном на цьому. Знайдіть відповідь Адораджана і подивіться, чи не стане життя після цього кращим.
користувач1567453

Відповіді:


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 ()) {" and not "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 для всіх знайдених мені екземплярів кнопки. Це набагато краще, ніж ручне встановлення слухача для всіх кнопок.
Особа

17
Проголосували. Це робить фрагменти багаторазовими. Інакше навіщо використовувати фрагменти?
буре

44
Хіба це не та сама методика, яку пропонували програмування Windows ще в 1987 році? Не хвилюватися. Google рухається швидко і стосується продуктивності розробників. Я впевнений, що не буде довго, поки обробка подіями не буде такою доброю, як 1991-eara Visual Basic.
Едвард Брей

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

4
@NathanOsman Я думаю, що питання стосувалося xml onClick, тому прийняті анс дають точне рішення.
Naveed Ahmad

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 () (Якщо я просто переходжу через перегляд діяльності, виклики to view.findViewById (R.id.myfragmentwidget_id) повертає нуль).
Майкл Нельсон

Це не працює з API 21 в моєму проекті. Будь-які думки щодо використання цього підходу?
Дарт Кодер

Його досить базовий код, який використовується майже в кожному додатку. Чи можете ви описати, що з вами відбувається?
Брілл Паппін

1
Запропонуйте цю відповідь для пояснення проблеми, яка сталася через те, що макет фрагмента додається до подання діяльності.
fllo

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 Canepa

@Aldo Для методу onClick в XML я вважаю, що вам слід мати андроїд: 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.Створіть такий BaseFragment:

public abstract class BaseFragment extends Fragment implements OnClickListener

2.Використання

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>

Також є дуже хороший підручник про DataBinding, який ви можете знайти тут .


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 ()). Якщо такий спосіб не знайдений у "Діяльності", система видасть виняток.


дякую, ніхто більше не пояснив, чому сталася аварія

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інтерфейс.

Крім того, вам не знадобляться будь-які заяви переключення.

На жаль, цей метод обмежений лише інтерфейсами, які потребують єдиного методу, або лямбда.


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реалізацію
  • Можна лише метод пошуку фрагмента "top-most" (тобто останнього ) у стеці (хоча це можна виправити, якщо потрібно)

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.