如何在Fragment中使用XML onClick处理按钮点击事件

460

在 Honeycomb (Android 3) 之前,每个 Activity 都是通过布局 XML 中的 onClick 标签来注册处理按钮点击事件的:

android:onClick="myClickMethod"

在该方法中,你可以使用view.getId()和switch语句来实现按钮逻辑。

随着Honeycomb的引入,我正在将这些Activity分解为Fragments,以便在许多不同的Activities中重用。大多数按钮的行为与Activity无关,并且我希望代码能够在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
    }
});

问题是当我的布局被填充时,接收到按钮点击事件的仍然是承载Activity而不是各自的Fragment。有没有很好的方法来:

  • 注册Fragment以接收按钮点击事件?
  • 将点击事件从Activity传递到它们所属的Fragment中?

1
你不能在片段的onCreate中处理注册监听器吗? - Jodes
26
是的,但我不想为每个按钮都使用setOnClickListenerfindViewById,这就是为什么添加onClick的原因,以使事情变得更加简单。 - smith324
4
看了最佳答案,我认为使用setOnClickListener比坚持使用XML onClick方法更松散耦合。如果活动必须将每次点击转发到正确的片段,则意味着每次添加片段都必须更改代码。使用接口从片段的基类解耦并不能解决这个问题。如果片段自己向正确的按钮注册,则活动保持完全不可知,这是更好的风格。请参见Adorjan Princz的答案。 - Adriaan Koster
@smith324必须在这个问题上同意Adriaan的看法。试试Adorjan的答案,看看之后生活是否会更好。 - user1567453
19个回答

619

我更喜欢使用以下解决方案来处理onClick事件。这适用于Activity和Fragments。

public class StartFragment extends Fragment implements OnClickListener{

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {

        View v = inflater.inflate(R.layout.fragment_start, container, false);

        Button b = (Button) v.findViewById(R.id.StartButton);
        b.setOnClickListener(this);
        return v;
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
        case R.id.StartButton:

            ...

            break;
        }
    }
}

9
在onCreateView中,我循环遍历ViewGroup v的所有子项,并为找到的所有Button实例设置onclicklistener。这比手动为所有按钮设置监听器要好得多。 - A Person
18
投票通过了。这使得片段可重复使用,否则为什么要使用片段呢? - boreas
47
这种技术不就是1987年《Windows编程》一书所倡导的吗?别担心,谷歌发展很快,专注于开发者的生产力。我相信不久之后事件处理将达到1991年 Visual Basic 的水平。 - Edward Brey
7
你使用了哪个导入类来处理OnClickListener?Intellij 建议使用android.view.View.OnClickListener,但它似乎没有起作用 :/ (onClick从未执行) - Lucas Jota
4
我想你在问与xml中的onClick相关的问题,所以被接受的答案提供了确切的解决方案。 - Naveed Ahmad
显示剩余18条评论

180
你可以这样做:
活动:
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希望降低耦合度以便Fragments能够重复使用的问题:

接口:

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

碎片(Fragment):

public class SomeFragment implements XmlClickable {

//...onCreateView, etc.

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

52
基本上这就是我现在正在做的事情,但是当你有多个需要接收点击事件的片段时,会变得非常混乱。总的来说,我对片段感到不满,因为它们周围的范例已经瓦解了。 - smith324
133
我遇到了同样的问题,虽然感谢你的回复,但从软件工程的角度来看,这不是一份干净的代码。这段代码导致活动与片段紧密耦合。应该能够在多个活动中重复使用相同的片段,而不需要活动了解片段的实现细节。 - Ameen
1
应该是"switch(v.getId()){"而不是"switch(v.getid()){"。 - eb80
5
你可以使用已经存在的OnClickListener,而不是定义自己的接口,正如Euporie所提到的那样。 - fr00tyl00p
1
@milad XmlClickable someFragment = (XmlClickable) getFragmentManager().findFragment.... - Blundell
显示剩余13条评论

31
我认为问题在于视图仍然是活动,而不是片段。 片段没有独立的视图,而是附加到父活动的视图上。 这就是为什么事件最终会在活动中结束而不是片段中。很不幸,但我认为您需要一些代码来解决这个问题。
在转换期间,我通常会添加一个单击监听器,该监听器调用旧的事件处理程序。
例如:
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(),以便它可以通过view.findViewById()访问片段的子视图,例如EditText(如果我只是简单地传递活动视图,则对view.findViewById(R.id.myfragmentwidget_id)的调用会返回null)。 - Michael Nelson
我的项目中API 21不支持这个方法。你有什么想法如何使用这种方法吗? - Darth Coder
这是相当基础的代码,在几乎每个应用程序中都会使用。你能描述一下它正在发生什么吗? - Brill Pappin
1
请给我点个赞,以表对这个答案的认可。该答案解释了由于片段的布局附加到活动视图而导致的问题。 - Blo

31

最近我解决了这个问题,而不必向上下文Activity添加方法或实现OnClickListener接口。我不确定这是否是一个“有效”的解决方案,但它可以运行。

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

使用数据绑定即可实现:只需将您的Fragment实例作为变量添加,然后就可以将任何方法与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
我只是有一种感觉,因为我无法让这个解决方案工作,所以可能缺少一些细节。 - Tima
也许你在文档的"构建环境"中遗漏了一些内容:https://developer.android.com/tools/data-binding/guide.html#binding_events - Aldo Canepa
@Aldo 对于 XML 中的 onClick 方法,我认为你应该使用 android:onClick="@{() -> fragment.buttonClicked()}"。此外,对于其他情况,你应该在片段内声明 buttonClicked() 函数并将逻辑放在其中。 - Hayk Nahapetyan
在xml中应该是 android:name="fragment" android:type="com.example.testapp.fragments.CustomFragment"/> - COYG
1
我刚试了一下,variable 标签的 nametype 属性不应该带有 android: 前缀。也许旧版本的 Android 布局需要它? - JohnnyLambada

7
我更倾向于在处理片段时直接使用代码中的点击处理,而不是在XML中使用onClick属性。
当将活动迁移到片段时,这变得更加容易。您可以直接从每个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:
              ...
        }
    }
}

当涉及到在片段中处理点击事件时,对我来说这看起来比android:onClick更简单。


7

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
这是另一种方法:

1. 创建一个类似于以下的 BaseFragment:

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

希望这能有所帮助。

除了不在每个Fragment类上重复实现OnClickListener之外,是否还有其他原因创建抽象的BaseFragment? - Dimitrios K.

5

在我的使用场景中,我有50多个ImageView需要连接到一个单独的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);
        }
    }

3

我看到的答案有些过时。最近,Google 推出了DataBinding,它比在 xml 中处理onClick或分配要容易得多。

这里有一个很好的例子,你可以看看如何处理:

<?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的非常好的教程。


3
你可以将回调函数定义为 XML 布局的属性。文章“Custom XML Attributes For Your Custom Android Widgets”将向你展示如何为自定义小部件执行此操作,Kevin Dion 做出了贡献 :)

我正在研究是否可以向 Fragment 基类添加可定制样式属性。

基本思路是在处理 onClick 回调时实现 View 所具有的相同功能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接