【问题标题】:How to handle button clicks using the XML onClick within Fragments如何在 Fragments 中使用 XML onClick 处理按钮点击
【发布时间】:2011-08-30 18:38:17
【问题描述】:

Honeycomb 之前 (Android 3),每个 Activity 都注册为通过布局 XML 中的 onClick 标记处理按钮点击:

android:onClick="myClickMethod"

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

随着 Honeycomb 的引入,我将这些活动分解为可以在许多不同活动中重复使用的片段。按钮的大部分行为是独立于 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?

【问题讨论】:

  • 你不能在片段的 onCreate 中处理注册监听器吗?
  • @jodes 是的,但我不想为每个按钮都使用setOnClickListenerfindViewById,这就是为什么添加了onClick,以使事情变得更简单。
  • 查看接受的答案,我认为使用 setOnClickListener 比坚持使用 XML onClick 方法更松散耦合。如果活动必须将每次点击“转发”到正确的片段,这意味着每次添加片段时代码都必须更改。使用接口与片段的基类分离并没有帮助。如果片段使用正确的按钮本身注册,则活动完全不可知,这是更好的 IMO 风格。另请参阅 Adorjan Princz 的答案。
  • @smith324 在这一点上必须同意 Adriaan。试一试 Adorjan 的回答,看看之后生活会不会好转。

标签: android xml button android-fragments


【解决方案1】:

我更喜欢使用以下解决方案来处理 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;
        }
    }
}

【讨论】:

  • 在 onCreateView 中,我遍历 ViewGroup v 的所有子项,并为我找到的所有 Button 实例设置 onclicklistener。这比手动设置所有按钮的监听器要好得多。
  • 已投票。这使得片段可重用。否则为什么要使用片段?
  • 这不是 Programming Windows 在 1987 年提倡的相同技术吗?不用担心。谷歌行动迅速,一切都与开发人员的生产力有关。我相信用不了多久,事件处理就会像 1991 年的 Visual Basic 一样好。
  • witch import 你用过OnClickListener吗? Intellij 建议我使用 android.view.View.OnClickListener 并且它不起作用:/(onClick 永远不会运行)
  • @NathanOsman 我认为 Question 与 xml onClick 有关,因此接受的 ans 提供了确切的解决方案。
【解决方案2】:

你可以这样做:

活动:

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

【讨论】:

  • 这就是我现在基本上正在做的事情,但是当您有多个片段,每个片段都需要接收点击事件时,它会变得更加混乱。总的来说,我只是对碎片感到恼火,因为范式已经在它们周围消失了。
  • 我遇到了同样的问题,尽管我很欣赏您的回复,但从软件工程的角度来看,这并不是干净的代码。此代码导致活动与片段紧密耦合。您应该能够在多个活动中重复使用相同的片段,而活动不知道片段的实现细节。
  • 应该是 "switch(v.getId()){" 而不是 "switch(v.getid()){"
  • 您可以使用 Euporie 提到的现有 OnClickListener,而不是定义自己的接口。
  • 当我读到这篇文章时,我差点哭了,这是太多的样板......来自@AdorjanPrincz 的以下答案是要走的路。
【解决方案3】:

我认为的问题是视图仍然是活动,而不是片段。片段没有任何自己的独立视图,并附加到父活动视图。这就是为什么事件最终出现在 Activity 中,而不是片段中的原因。很不幸,但我认为您需要一些代码才能完成这项工作。

我在转换期间所做的只是添加一个调用旧事件处理程序的点击侦听器。

例如:

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

【讨论】:

  • 谢谢 - 我使用它进行了一点修改,因为我将片段视图(即 inflater.inflate(R.layout.my_fragment_xml_resource) 的结果)传递给 onLoginClicked() 以便它可以通过 view.findViewById() 访问片段子视图,例如 EditText(如果我只是通过活动视图,对 view.findViewById(R.id.myfragmentwidget_id) 的调用返回 null)。
  • 这不适用于我的项目中的 API 21。关于如何使用这种方法的任何想法?
  • 它非常基本的代码,几乎用于所有应用程序。你能描述一下你正在发生的事情吗?
  • 对这个答案投赞成票,以解释由于片段的布局附加到活动视图而发生的问题。
【解决方案4】:

我最近解决了这个问题,无需向上下文 Activity 添加方法或实现 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;
    }

    ...

}

【讨论】:

  • 我只是有一种感觉,因为我无法让这个解决方案发挥作用,所以缺少一些细节
  • 也许您在文档中的“构建环境”中遗漏了一些内容:developer.android.com/tools/data-binding/…
  • @Aldo 对于 XML 中的 onClick 方法,我相信您应该使用 android:onClick="@{() -> fragment.buttonClicked()}" 代替。同样对于其他人,您应该在片段中声明 buttonClicked() 函数并将您的逻辑放入其中。
  • 在xml中应该是android:name="fragment" android:type="com.example.testapp.fragments.CustomFragment"/&gt;
  • 我刚试过这个,variable 标签的nametype 属性应该android: 前缀。也许有一个旧版本的 Android 布局确实需要它?
【解决方案5】:

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】:

    在处理片段时,我宁愿在代码中进行点击处理,也不愿在 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】:

      这是另一种方式:

      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天:除了不在每个Fragment类上重复实现OnClickListener来创建抽象BaseFragment之外,还有什么原因吗?
      【解决方案8】:

      在我的用例中,我需要将 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);
              }
          }
      

      【讨论】:

        【解决方案9】:

        当我看到答案时,它们有点老了。最近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教程,你可以在Here找到它。

        【讨论】:

          【解决方案10】:

          您可以将回调定义为 XML 布局的属性。文章 Custom XML Attributes For Your Custom Android Widgets 将向您展示如何为自定义小部件执行此操作。归功于凯文迪翁:)

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

          基本思想是与 View 在处理 onClick 回调时实现的功能相同。

          【讨论】:

            【解决方案11】:

            补充布伦德尔的答案,
            如果你有更多的片段,有大量的 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
                 }
              } 
            

            【讨论】:

              【解决方案12】:

              如果您使用 android:Onclick="" 在 xml 中注册,回调将被提供给您的片段所属上下文的受尊重的 Activity (getActivity() )。如果Activity中没有找到这样的方法,系统会抛出异常。

              【讨论】:

              • 谢谢,没有其他人解释为什么会发生崩溃
              【解决方案13】:

              您可能需要考虑将 EventBus 用于解耦事件 .. 您可以非常轻松地监听事件。您还可以确保在 ui 线程上接收到事件(而不是为每个事件订阅为自己调用 runOnUiThread..)

              https://github.com/greenrobot/EventBus

              来自 Github:

              Android 优化的事件总线,简化了之间的通信 活动、片段、线程、服务等。代码越少越好 质量

              【讨论】:

              • 不是一个完美的解决方案
              • @blueware 请详细说明
              【解决方案14】:

              我想添加到 Adjorn Linkz 的 answer

              如果您需要多个处理程序,您可以使用 lambda 引用

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

              这里的技巧是handler 方法的签名匹配View.OnClickListener.onClick 签名。这样,您将不需要View.OnClickListener 接口。

              此外,您不需要任何 switch 语句。

              遗憾的是,此方法仅限于需要单个方法或 lambda 的接口。

              【讨论】:

                【解决方案15】:

                虽然我发现了一些依赖于数据绑定的好答案,但我并没有看到这种方法能完全发挥作用——在启用片段解析同时允许 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 实现
                • 只能在堆栈中查找“最顶层”(即最后一个)片段的方法(尽管可以修复,如果需要的话)

                【讨论】:

                  【解决方案16】:

                  这一直为我工作:(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
                  

                  【讨论】:

                  【解决方案17】:

                  最佳解决方案恕我直言:

                  在片段中:

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

                  然后在Fragment的onViewStateRestored:

                  addClick(R.id.myButton);
                  

                  【讨论】:

                    【解决方案18】:

                    您的 Activity 正在接收回调,必须使用:

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

                    如果您希望片段接收回调,请执行以下操作:

                    mViewPagerCloth.setOnClickListener(this);
                    

                    并在 Fragment 上实现onClickListener 接口

                    【讨论】:

                      【解决方案19】:

                      以下解决方案可能是一个更好的解决方案。布局在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>
                      

                      片段如下

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

                      【讨论】:

                        猜你喜欢
                        • 2013-01-24
                        • 1970-01-01
                        • 1970-01-01
                        • 2011-06-26
                        • 1970-01-01
                        • 2012-08-20
                        • 1970-01-01
                        • 1970-01-01
                        相关资源
                        最近更新 更多