Pre-Honeycomb (Android 3),每个Activity都注册为通过布局XML中的onClick标签处理按钮点击:
android:onClick="myClickMethod"
在该方法中,您可以使用view.getId()和switch语句来执行按钮逻辑。
随着蜂窝的引入,我将这些活动分解为片段,这些片段可以在许多不同的活动中重用。按钮的大部分行为是活动独立的,我想代码驻留在Fragments文件中,而不使用旧的(pre 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
}
});
问题是,当我的布局被膨胀时,它仍然是接收按钮点击的宿主活动,而不是单个片段。有没有好的方法来解决这两个问题
注册片段以接收按钮点击?
将点击事件从活动传递到它们所属的片段?
尽管我已经发现了一些依赖于数据绑定的不错的答案,但我还没有看到这种方法能达到完全的程度——在支持片段解析的同时允许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的“现代”实现
只能查找堆栈中“最顶层”(即最后一个)片段的方法(尽管这可以修复,如果需要)
这是另一种方式:
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
and
BaseFragment fragment = new FragmentA;
public void onClick(View v){
fragment.onClick(v);
}
希望能有所帮助。
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);
}
}
}
}