简体   繁体   English

如何在 Fragments 中使用 XML onClick 处理按钮点击

[英]How to handle button clicks using the XML onClick within Fragments

Pre-Honeycomb (Android 3), each Activity was registered to handle button clicks via the onClick tag in a Layout's XML: Pre-Honeycomb (Android 3),每个 Activity 都通过布局 XML 中的onClick标签注册来处理按钮点击:

android:onClick="myClickMethod"

Within that method you can use view.getId() and a switch statement to do the button logic.在该方法中,您可以使用view.getId()和 switch 语句来执行按钮逻辑。

With the introduction of Honeycomb I'm breaking these Activities into Fragments which can be reused inside many different Activities.随着 Honeycomb 的引入,我将这些活动分解为可以在许多不同活动中重复使用的片段。 Most of the behavior of the buttons is Activity independent, and I would like the code to reside inside the Fragments file without using the old (pre 1.6) method of registering the OnClickListener for each button.按钮的大部分行为与活动无关,我希望代码驻留在 Fragments 文件中,而不使用为每个按钮注册OnClickListener的旧(1.6 版之前)方法。

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

The problem is that when my layout's are inflated it is still the hosting Activity that is receiving the button clicks, not the individual Fragments.问题是,当我的布局膨胀时,它仍然是接收按钮点击的宿主活动,而不是单个片段。 Is there a good approach to either有没有好的方法

  • Register the fragment to receive the button clicks?注册片段以接收按钮点击?
  • Pass the click events from the Activity to the fragment they belong to?将 Activity 中的点击事件传递给它们所属的片段?

I prefer using the following solution for handling onClick events.我更喜欢使用以下解决方案来处理 onClick 事件。 This works for Activity and Fragments as well.这也适用于活动和片段。

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

You could just do this:你可以这样做:

Activity:活动:

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

In response to @Ameen who wanted less coupling so Fragments are reuseable回应希望减少耦合的@Ameen,因此 Fragments 可重用

Interface:界面:

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

Activity:活动:

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

The problem I think is that the view is still the activity, not the fragment.我认为的问题是视图仍然是活动,而不是片段。 The fragments doesn't have any independent view of its own and is attached to the parent activities view.这些片段没有自己的任何独立视图,并附加到父活动视图。 Thats why the event ends up in the Activity, not the fragment.这就是为什么事件最终出现在活动中,而不是片段中。 Its unfortunate, but I think you will need some code to make this work.不幸的是,但我认为您需要一些代码来完成这项工作。

What I've been doing during conversions is simply adding a click listener that calls the old event handler.我在转换过程中所做的只是添加一个调用旧事件处理程序的点击侦听器。

for instance:例如:

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

I've recently solved this issue without having to add a method to the context Activity or having to implement OnClickListener.我最近解决了这个问题,而无需向上下文 Activity 添加方法或实现 OnClickListener。 I'm not sure if it is a "valid" solution neither, but it works.我不确定它是否是一个“有效”的解决方案,但它有效。

Based on: https://developer.android.com/tools/data-binding/guide.html#binding_events基于: https : //developer.android.com/tools/data-binding/guide.html#binding_events

It can be done with data bindings: Just add your fragment instance as a variable, then you can link any method with onClick.它可以通过数据绑定来完成:只需将片段实例添加为变量,然后您就可以将任何方法与 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>

And the fragment linking code would be...片段链接代码将是......

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

    ...

}

I would rather go for the click handling in code than using the onClick attribute in XML when working with fragments.在处理片段时,我宁愿在代码中进行点击处理,也不愿在 XML 中使用onClick属性。

This becomes even easier when migrating your activities to fragments.将您的活动迁移到片段时,这变得更加容易。 You can just call the click handler (previously set to android:onClick in XML) directly from each case block.您可以直接从每个case块调用单击处理程序(以前在 XML 中设置为android:onClick )。

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

When it comes to handling clicks in fragments, this looks simpler to me than android:onClick .在处理片段中的点击时,这对我来说看起来比android:onClick简单。

ButterKnife is probably the best solution for the clutter problem. ButterKnife可能是解决杂乱问题的最佳解决方案。 It uses annotation processors to generate the so called "old method" boilerplate code.它使用注释处理器生成所谓的“旧方法”样板代码。

But the onClick method can still be used, with a custom inflator.但是 onClick 方法仍然可以使用,带有自定义充气器。

How to use如何使用

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

Implementation执行

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

This is another way:这是另一种方式:

1.Create a BaseFragment like this: 1.像这样创建一个BaseFragment:

public abstract class BaseFragment extends Fragment implements OnClickListener

2.Use 2.使用

public class FragmentA extends BaseFragment 

instead of代替

public class FragmentA extends Fragment

3.In your activity: 3.在您的活动中:

public class MainActivity extends ActionBarActivity implements OnClickListener

and

BaseFragment fragment = new FragmentA;

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

Hope it helps.希望能帮助到你。

In my use case, I have 50 odd ImageViews I needed to hook into a single onClick method.在我的用例中,我需要将 50 个奇数的 ImageView 挂接到单个 onClick 方法中。 My solution is to loop over the views inside the fragment and set the same onclick listener on each:我的解决方案是遍历片段内的视图并在每个视图上设置相同的 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);
        }
    }

As I see answers they're somehow old.当我看到答案时,它们有些老了。 Recently Google introduce DataBinding which is much easier to handle onClick or assigning in your xml.最近Google引入了DataBinding ,它更容易在您的 xml 中处理onClick或分配。

Here is good example which you can see how to handle this :这是一个很好的例子,你可以看到如何处理这个:

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

There is also very nice tutorial about DataBinding you can find it Here .还有关于 DataBinding 的非常好的教程,您可以在这里找到它。

You can define a callback as an attribute of your XML layout.您可以将回调定义为 XML 布局的属性。 The article Custom XML Attributes For Your Custom Android Widgets will show you how to do it for a custom widget.文章为您的自定义 Android 小部件自定义 XML 属性将向您展示如何为自定义小部件执行此操作。 Credit goes to Kevin Dion :)归功于凯文·迪翁 :)

I'm investigating whether I can add styleable attributes to the base Fragment class.我正在研究是否可以向基本 Fragment 类添加可样式化的属性。

The basic idea is to have the same functionality that View implements when dealing with the onClick callback.基本思想是在处理 onClick 回调时具有与 View 相同的功能。

Adding to Blundell's answer,添加到 Blundell 的回答中,
If you have more fragments, with plenty of onClicks:如果你有更多的片段,有很多 onClicks:

Activity:活动:

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

} 

In Your Fragment:在你的片段中:

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

If you register in xml using android:Onclick="", callback will be given to the respected Activity under whose context your fragment belongs to (getActivity() ).如果您使用 android:Onclick="" 在 xml 中注册,回调将提供给您的片段所属上下文的受尊重活动 (getActivity())。 If such method not found in the Activity, then system will throw an exception.如果在 Activity 中找不到这样的方法,那么系统将抛出异常。

You might want to consider using EventBus for decoupled events .. You can listen for events very easily.您可能需要考虑将 EventBus 用于解耦事件。您可以非常轻松地侦听事件。 You can also make sure the event is being received on the ui thread (instead of calling runOnUiThread.. for yourself for every event subscription)您还可以确保在 ui 线程上接收到事件(而不是为每个事件订阅调用 runOnUiThread.. )

https://github.com/greenrobot/EventBus https://github.com/greenrobot/EventBus

from Github:来自 Github:

Android optimized event bus that simplifies communication between Activities, Fragments, Threads, Services, etc. Less code, better quality Android 优化的事件总线,简化了活动、片段、线程、服务等之间的通信。更少的代码,更好的质量

I'd like to add to Adjorn Linkz's answer .我想补充 Adjorn Linkz 的回答

If you need multiple handlers, you could just use lambda references如果您需要多个处理程序,您可以只使用 lambda 引用

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

The trick here is that handler method's signature matches View.OnClickListener.onClick signature.这里的技巧是handler方法的签名与View.OnClickListener.onClick签名匹配。 This way, you won't need the View.OnClickListener interface.这样,您就不需要View.OnClickListener接口。

Also, you won't need any switch statements.此外,您不需要任何 switch 语句。

Sadly, this method is only limited to interfaces that require a single method, or a lambda.遗憾的是,此方法仅限于需要单个方法或 lambda 的接口。

Though I've spotted some nice answers relying on data binding, I didn't see any going to the full extent with that approach -- in the sense of enabling fragment resolution while allowing for fragment-free layout definitions in XML's.尽管我发现了一些依赖于数据绑定的不错的答案,但我没有看到任何使用这种方法的全部内容——在启用片段解析的意义上,同时允许 XML 中的无片段布局定义。

So assuming data binding is enabled, here's a generic solution I can propose;因此,假设数据启用绑定,这里有一个通用的解决方案,我可以建议; A bit long but it definitely works (with some caveats):有点长,但它绝对有效(有一些警告):

Step 1: Custom OnClick Implementation第 1 步:自定义 OnClick 实现

This will run a fragment-aware search through contexts associated with the tapped-on view (eg button):这将通过与点击视图(例如按钮)相关联的上下文运行片段感知搜索:


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

Note: loosely based on the original Android implementation (see https://android.googlesource.com/platform/frameworks/base/+/a175a5b/core/java/android/view/View.java#3025 )注意:松散地基于原始 Android 实现(参见https://android.googlesource.com/platform/frameworks/base/+/a175a5b/core/java/android/view/View.java#3025

Step 2: Declarative application in layout files第 2 步:布局文件中的声明式应用程序

Then, in data-binding aware XML's:然后,在数据绑定感知 XML 中:

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

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

Caveats注意事项

  • Assumes a 'modern' FragmentActivity based implementation假设一个基于“现代” FragmentActivity的实现
  • Can only lookup method of "top-most" (ie last ) fragment in stack (though that can be fixed, if need be)只能在堆栈中查找“最顶层”(即最后一个)片段的方法(尽管可以修复,如果需要的话)

This has been working for me:(Android 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

Best solution IMHO:恕我直言,最佳解决方案:

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

then in Fragment's onViewStateRestored:然后在 Fragment 的 onViewStateRestored 中:

addClick(R.id.myButton);

Your Activity is receiving the callback as must have used:您的 Activity 正在接收必须使用的回调:

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

If you want your fragment to receive callback then do this:如果您希望片段接收回调,请执行以下操作:

mViewPagerCloth.setOnClickListener(this);

and implement onClickListener interface on Fragment并在 Fragment 上实现onClickListener接口

The following solution might be a better one to follow.以下解决方案可能是更好的解决方案。 the layout is in fragment_my.xml布局在fragment_my.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="listener"
            type="my_package.MyListener" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        
        <Button
            android:id="@+id/moreTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> listener.onClick()}"
            android:text="@string/login"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

And the Fragment would be as follows片段如下

class MyFragment : Fragment(), MyListener {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
            return FragmentMyBinding.inflate(
                inflater,
                container,
                false
            ).apply {
                lifecycleOwner = viewLifecycleOwner
                listener = this@MyFragment
            }.root
    }

    override fun onClick() {
        TODO("Not yet implemented")
    }

}

interface MyListener{
    fun onClick()
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM