如何禁用Android应用所有视图的拖拽功能并保留指定特性?
在Google Play Console的Errors and ANR区域,搭载Android 10及更高版本的用户设备频繁出现如下异常:
Exception java.lang.IllegalStateException: Drag shadow dimensions must
be positive at android.view.View.startDragAndDrop (View.java:25464)
at android.widget.Editor.startDragAndDrop (Editor.java:1183) at
android.widget.Editor.performLongClick (Editor.java:1209) at
android.widget.TextView.performLongClick (TextView.java:12292) at
android.view.View.performLongClick (View.java:7222) at
android.view.View$CheckForLongPress.run (View.java:27374) at
android.os.Handler.handleCallback (Handler.java:883) at
android.os.Handler.dispatchMessage (Handler.java:100) at
android.os.Looper.loop (Looper.java:241) at
android.app.ActivityThread.main (ActivityThread.java:7617) at
java.lang.reflect.Method.invoke at
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run
(RuntimeInit.java:492) at com.android.internal.os.ZygoteInit.main
(ZygoteInit.java:941)
经排查,该异常由TextView触发拖拽操作导致,对应堆栈中的android.widget.TextView.performLongClick (TextView.java:12292)行。
需求说明
不需要拖拽(Drag and Drop)功能,但需保留TextView的以下特性:
- 选择部分或全部文本进行复制、粘贴、分享;
- 通过继承自
TextView的自定义类ZoomTextView实现捏合缩放调整文本大小; - 支持可点击链接。
此外,应用中使用的RecyclerView、菜单等视图也需要禁用拖拽功能,希望全局禁用应用内所有视图的拖拽功能。
现有代码
Fragment内TextView配置代码
public class MyFragment extends Fragment implements TextToSpeechCallback { private ZoomTextView mTextView; private void setConfiguration() { mViewModel = new ViewModelProvider(this).get(MyViewModel.class); mTextView = binding.include.tvZoomable; SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); float fontSize = Float.parseFloat(prefs.getString("font_size", "18")); String fontFamily = String.format(new Locale("es"),"fonts/%s",prefs.getString("font_name", "robotoslab_regular.ttf")); Typeface tf= Typeface.createFromAsset(requireActivity().getAssets(),fontFamily); mTextView .setTypeface(tf); mTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize); hasInvitatory = prefs.getBoolean("invitatorio", false); isVoiceOn = prefs.getBoolean("voice", true); if (isVoiceOn) { sbReader = new StringBuilder(VOICE_INI); } } // ... }
TextView的XML布局
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/root_preferences" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context=".ui.activities.MainActivity" > <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.utils.ZoomTextView android:id="@+id/tv_Zoomable" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="0dp" android:paddingBottom="50dp" style="@style/TextAppearance.AppTheme.DisplayBody" /> </ScrollView> </androidx.constraintlayout.widget.ConstraintLayout>
提问
是否可以通过XML或编程方式禁用应用内所有视图的拖拽功能?
一、针对TextView的精准禁用(优先推荐)
异常由TextView长按触发拖拽导致,且需保留文本选择、缩放、链接点击功能,直接在自定义ZoomTextView中重写拖拽相关方法即可:
public class ZoomTextView extends TextView { // 已有构造方法和捏合缩放逻辑... @Override public boolean startDragAndDrop(ClipData data, DragShadowBuilder shadowBuilder, Object myLocalState, int flags) { // 返回false阻止拖拽启动,不影响文本选择等功能 return false; } // 兼容旧版本拖拽API @Override public boolean startDrag(ClipData data, DragShadowBuilder shadowBuilder, Object myLocalState, int flags) { return false; } }
此方式仅拦截TextView的拖拽触发,完全保留你需要的所有特性——文本选择、缩放、链接点击的逻辑与拖拽分离,不受影响。
二、全局禁用所有视图的拖拽功能
1. 自定义Application拦截全局视图
创建自定义Application类,通过LayoutInflater.Factory2拦截所有视图的拖拽逻辑:
public class MyApp extends Application { @Override public void onCreate() { super.onCreate(); LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() { @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { View view = getDelegate().createView(parent, name, context, attrs); if (view != null) { disableViewDrag(view); } return view; } @Override public View onCreateView(String name, Context context, AttributeSet attrs) { return onCreateView(null, name, context, attrs); } }); } private void disableViewDrag(View view) { // 给TextView以外的视图设置长按监听,消费事件阻止拖拽 if (!(view instanceof TextView)) { view.setOnLongClickListener(v -> true); } // 重写视图的拖拽方法做双重拦截 try { Method method = View.class.getDeclaredMethod("startDragAndDrop", ClipData.class, DragShadowBuilder.class, Object.class, int.class); method.setAccessible(true); // 通过动态代理替换方法实现(可选,若仅拦截长按不够彻底时使用) } catch (NoSuchMethodException e) { e.printStackTrace(); } } }
需在AndroidManifest.xml中指定自定义Application:
<application android:name=".MyApp" <!-- 其他配置 --> > </application>
2. 全局样式设置(不推荐)
在全局主题中添加android:longClickable="false",但会导致TextView无法触发长按文本选择,不符合需求,仅适用于无长按需求的场景。
三、RecyclerView与菜单的拖拽禁用
- RecyclerView:若通过
ItemTouchHelper实现拖拽,直接移除对应的ItemTouchHelper.Callback即可;系统默认拖拽则给子项设置setOnLongClickListener(v -> true)。 - 菜单:系统菜单默认无拖拽功能,若为自定义拖拽菜单,直接移除拖拽逻辑即可。
内容的提问来源于stack exchange,提问作者A. Cedano

