【Android】从事件分发开始:原理解析如何解决滑动冲突
【Android】从事件分发开始:原理解析如何解决滑动冲突
文章目录
- 【Android】从事件分发开始:原理解析如何解决滑动冲突
- Activity层级结构
- 浅析Activity的setContentView源码
- 浅析AppCompatActivity的setContentView源码
- 触控三分显纷争,滑动冲突待分明
- 触控起源定归途
- 事件分发机制
- 点击事件分发的传递规则
- 产生滑动冲突的原因
- 拦截消冲理滑争
- **`ACTION_DOWN` 事件的处理**
- **`ACTION_MOVE` 事件的处理**
- **`ACTION_CANCEL` 事件的触发**
- **事件传递过程的重置**
- EasyTry嵌套,强制拦截
- NestedScrollableHost嵌套,选择性拦截
- 结语
参考:
《Android进阶之光》
【Android】图解View事件分发机制_android view 事件分发-CSDN博客
Activity层级结构
要知道Activity的层级结构,首先我们来看看Activity中总是出现的setContentView方法是干啥的。
浅析Activity的setContentView源码
点进Activity的setContentView。
public void setContentView(@LayoutRes int layoutResID) {getWindow().setContentView(layoutResID);initWindowDecorActionBar();
}
这里调用了 getWindow().setContentView(layoutResID)。getWindow() 会得到什么呢?接着往下看,getWindow() 返回 mWindow:
public Window getWindow() {return mWindow;
}
那么,这里的 mWindow 又是什么呢?我们继续查看代码,最终在 Activity 的 attach 方法中发现了 mWindow。
final void attach(Context context, ActivityThread aThread,Instrumentation instr, IBinder token, int ident,Application application, Intent intent, ActivityInfo info,CharSequence title, Activity parent, String id,NonConfigurationInstances lastNonConfigurationInstances,Configuration config, String referrer, VoiceInteractor voiceInteractor) {attachBaseContext(context);fragments.attach(null /* opaque */);mWindow = new PhoneWindow(this);...
}
由此可见,getWindow() 返回的是 PhoneWindow。下面来看看 PhoneWindow 的 setContentView 方法,代码如下所示:
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {if (mContentParent == null) {installDecor();} else if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {mContentParent.removeAllViews();}if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {view.setLayoutParams(params);final Scene newScene = new Scene(mContentParent, view);transitionTo(newScene);} else {mContentParent.addView(view, params);}
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {cb.onContentChanged();
}
原来 mWindow 指的就是 PhoneWindow,而 PhoneWindow 继承自抽象类 Window,这样就知道了getWindow() 得到的是一个 PhoneWindow,因为 Activity 中 setContentView 方法调用的是getWindow().setContentView(layoutResID)。
我们接着往下看,看看上面代码注释 1 处 installDecor 方法里面做了什么,代码如下所示:
private void installDecor() {if (mDecor == null) {mDecor = generateLayout();}
}
在前面的代码中没发现什么,紧接着查看上面代码注释 1 处的 generateDecor 方法里做了什么:
protected DecorView generateDecor() {return new DecorView(getContext(), -1);
}
这里创建了一个 DecorView。这个 DecorView 就是 Activity 中的根 View。接着查看 DecorView 的源码,发现 DecorView 是 PhoneWindow 类的内部类,并且继承了 FrameLayout。我们再回到 installDecor 方法中,查看 generateLayout(mDecor) 做了什么:
protected ViewGroup generateLayout(DecorView decor) {//根据不同的情况,给 layoutResource 加载不同的布局int layoutResource;int features = getLocalFeatures();if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {layoutResource = R.layout.screen_swipe_dismiss;} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {if (isFloating()) {TypedValue res = new TypedValue();getContext().getTheme().resolveAttribute(R.attr.dialogTitleDecorLayout, res, true);layoutResource = res.resourceId;} else {layoutResource = R.layout.screen_title_icons;}removeFeatures(FEATURE_LEFT_ICON);removeFeatures(FEATURE_RIGHT_ICON);} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0&& (features & (1 << FEATURE_ACTION_BAR_OVERLAY)) == 0) {layoutResource = R.layout.screen_action_bar;} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {if (isFloating()) {TypedValue res = new TypedValue();getContext().getTheme().resolveAttribute(R.attr.dialogTitleDecorLayout, res, true);layoutResource = res.resourceId;} else {layoutResource = R.layout.screen_title;}removeFeatures(FEATURE_ACTION_BAR);} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {layoutResource = R.styleable.Window_windowActionBarFullscreenDecorLayout,R.layout.screen_action_bar;} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {layoutResource = R.layout.screen_simple;} else {layoutResource = R.layout.screen_simple;}return contentParent;
}
PhoneWindow 的 generateLayout 方法比较长,这里只截取了一小部分关键的代码,其主要内容就是,根据不同的情况给 layoutResource 加载不同的布局。现在查看上面代码注释 1 处的布局 R.layout.screen_title,这个文件在 frameworks 源码中,它的代码如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:fitsSystemWindows="true"><!— Popup bar for action modes --><ViewStub android:id="@+id/action_mode_bar_stub"android:inflatedLayout="@layout/action_mode_bar"android:layout_width="match_parent"android:layout_height="wrap_content" /><FrameLayoutandroid:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1"style="?android:attr/windowTitleSizeStyle"><TextView android:id="@android:id/title"style="?android:attr/windowTitleStyle"android:background="?null"android:fadingEdge="horizontal" /></FrameLayout>
</LinearLayout>
上面的 ViewStub 是用来显示 ActionMode 的。下面的两个 FrameLayout:一个是 title,用来显示标题;另一个是 content,用来显示内容。看到上面的源码,大家就知道了一个 Activity 包含一个 Window 对象,该对象是由 PhoneWindow 实现的。PhoneWindow 将 DecorView 作为整个应用窗口的根 View,这个 DecorView 又将屏幕分为两个区域:一个区域是 TitleView,另一个区域是 ContentView,而我们平常做应用所写的布局正是显示在 ContentView 中的,如下图。

浅析AppCompatActivity的setContentView源码
我们在开发过程中用的基本都是AppCompatActivity,那么让我们看看AppCompatActivity的setContentView方法跟Activity的setContentView方法有什么不同。
首先来回忆以下Activity的setContentView方法中有两个很重要的东西:mDecor和mContentParent。前者是整个界面的根部局,后者是加载我们自定义布局的容器。其实AppCompatActivity的布局的层级跟Activity基本是一样的,只是在mContentParent中又多了一层布局而已。
话不多说,点开AppCompatActivity的setContentView方法:
@Override
public void setContentView(@LayoutRes int layoutResID) {getDelegate().setContentView(layoutResID);
}@NonNull
public AppCompatDelegate getDelegate() {if (mDelegate == null) {mDelegate = AppCompatDelegate.create(this, this);}return mDelegate;
}public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {return new AppCompatDelegateImpl(activity, activity.getWindow(), callback);
}
可以看到,里面是调用了一个委托对象的setContentView方法,这个对象就是AppCompatDelegateImpl,那么我们找到AppCompatDelegateImpl的setContentView方法:
@Override
public void setContentView(int resId) {ensureSubDecor();ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);contentParent.removeAllViews();LayoutInflater.from(mContext).inflate(resId, contentParent);mOriginalWindowCallback.onContentChanged();
}
明白了Activity布局加载的过程我们可以猜到,方法中第二行获取到的contentParent的功能就相当于Activity中mContentParent,我们自定义的布局就是加载到这个contentParent中的,接下来的代码LayoutInflater.from(mContext).inflate(resId, contentParent)马上就证实了我们的判断。
那么第一行的ensureSubDecor()是干啥的呢?在开篇的时候说过,AppCompatActivity的布局的层级跟Activity基本是一样的,只是在mContentParent中又多了一层布局。这里提前给出结论:mContentParent中又多的那一层布局就是mSubDecor,而contentParent又是在mSubDecor下面的一个子布局。
点进ensureSubDecor方法:
private void ensureSubDecor() {if (!mSubDecorInstalled) {mSubDecor = createSubDecor();……}
可以看到通过createSubDecor方法创建mSubDecor。
private ViewGroup createSubDecor() {TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {a.recycle();throw new IllegalStateException("You need to use a Theme.AppCompat theme (or descendant) with this activity.");}if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {requestWindowFeature(Window.FEATURE_NO_TITLE);} else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {// Don't allow an action bar if there is no title.requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);}if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) {requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);}if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) {requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY);}mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);a.recycle();// Now let's make sure that the Window has installed its decor by retrieving itensureWindow();//注释1mWindow.getDecorView();final LayoutInflater inflater = LayoutInflater.from(mContext);ViewGroup subDecor = null;//根据设置给subDecor加载不同的布局if (!mWindowNoTitle) {if (mIsFloating) {// If we're floating, inflate the dialog title decorsubDecor = (ViewGroup) inflater.inflate(R.layout.abc_dialog_title_material, null);// Floating windows can never have an action bar, reset the flagsmHasActionBar = mOverlayActionBar = false;} else if (mHasActionBar) {/*** This needs some explanation. As we can not use the android:theme attribute* pre-L, we emulate it by manually creating a LayoutInflater using a* ContextThemeWrapper pointing to actionBarTheme.*/TypedValue outValue = new TypedValue();mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);Context themedContext;if (outValue.resourceId != 0) {themedContext = new ContextThemeWrapper(mContext, outValue.resourceId);} else {themedContext = mContext;}// Now inflate the view using the themed context and set it as the content viewsubDecor = (ViewGroup) LayoutInflater.from(themedContext).inflate(R.layout.abc_screen_toolbar, null);mDecorContentParent = (DecorContentParent) subDecor.findViewById(R.id.decor_content_parent);mDecorContentParent.setWindowCallback(getWindowCallback());/*** Propagate features to DecorContentParent*/if (mOverlayActionBar) {mDecorContentParent.initFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);}if (mFeatureProgress) {mDecorContentParent.initFeature(Window.FEATURE_PROGRESS);}if (mFeatureIndeterminateProgress) {mDecorContentParent.initFeature(Window.FEATURE_INDETERMINATE_PROGRESS);}}} else {if (mOverlayActionMode) {subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple_overlay_action_mode, null);} else {subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);}if (Build.VERSION.SDK_INT >= 21) {// If we're running on L or above, we can rely on ViewCompat's// setOnApplyWindowInsetsListenerViewCompat.setOnApplyWindowInsetsListener(subDecor,new OnApplyWindowInsetsListener() {@Overridepublic WindowInsetsCompat onApplyWindowInsets(View v,WindowInsetsCompat insets) {final int top = insets.getSystemWindowInsetTop();final int newTop = updateStatusGuard(top);if (top != newTop) {insets = insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(),newTop,insets.getSystemWindowInsetRight(),insets.getSystemWindowInsetBottom());}// Now apply the insets on our viewreturn ViewCompat.onApplyWindowInsets(v, insets);}});} else {// Else, we need to use our own FitWindowsViewGroup handling((FitWindowsViewGroup) subDecor).setOnFitSystemWindowsListener(new FitWindowsViewGroup.OnFitSystemWindowsListener() {@Overridepublic void onFitSystemWindows(Rect insets) {insets.top = updateStatusGuard(insets.top);}});}}if (subDecor == null) {throw new IllegalArgumentException("AppCompat does not support the current theme features: { "+ "windowActionBar: " + mHasActionBar+ ", windowActionBarOverlay: "+ mOverlayActionBar+ ", android:windowIsFloating: " + mIsFloating+ ", windowActionModeOverlay: " + mOverlayActionMode+ ", windowNoTitle: " + mWindowNoTitle+ " }");}if (mDecorContentParent == null) {mTitleView = (TextView) subDecor.findViewById(R.id.title);}// Make the decor optionally fit system windows, like the window's decorViewUtils.makeOptionalFitsSystemWindows(subDecor);//注释2final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(R.id.action_bar_activity_content);final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);if (windowContentView != null) {// There might be Views already added to the Window's content view so we need to// migrate them to our content viewwhile (windowContentView.getChildCount() > 0) {final View child = windowContentView.getChildAt(0);windowContentView.removeViewAt(0);contentView.addView(child);}// Change our content FrameLayout to use the android.R.id.content id.// Useful for fragments.//清除windowContentView的idwindowContentView.setId(View.NO_ID);//将contentView的id设置成android.R.id.contentcontentView.setId(android.R.id.content);// The decorContent may have a foreground drawable set (windowContentOverlay).// Remove this as we handle it ourselvesif (windowContentView instanceof FrameLayout) {((FrameLayout) windowContentView).setForeground(null);}}// Now set the Window's content view with the decor//注释3mWindow.setContentView(subDecor);contentView.setAttachListener(new ContentFrameLayout.OnAttachListener() {@Overridepublic void onAttachedFromWindow() {}@Overridepublic void onDetachedFromWindow() {dismissPopups();}});return subDecor;}
在注释1处调用了mWindow的getDecorView方法,这里的mWindow就是PhoneWindow了,在PhoneWinow中找到getDecorView方法:
@Overridepublic final View getDecorView() {if (mDecor == null || mForceDecorInstall) {installDecor();}return mDecor;}
可以看到里面调用了installDecor方法,在前面的文章里提过,做的事情就是初始化mDecor和mContentParent。接下来是根据设置给subDecor加载不同的布局。再接着,在注释2处,通过subDecor的findViewById(R.id.action_bar_activity_content)方法获取到了id为R.id.action_bar_activity_content的contentView,然后再通过mWindow的findViewById(android.R.id.content)方法获取到了windowContentView(对应着Activity中的mContentParent),接着通过windowContentView.setId(View.NO_ID)方法将windowContentView的id清除,之后再用contentView.setId(android.R.id.content)将contentView的id设为android.R.id.content。这样一来,contentView 就成为了Activity中的mContentParent,我们编写的的布局加载到contentView中。
最后在注释3处,调用了PhoneWindow的setContentView(subDecor)方法将创建的subDecor放到mContentParent中,但是此时的mContentParent已经不是加载我们自己编写布局的那个容器了,加载我们编写的布局的容器已经变成了subDecor中的contentView。
让我们再回到AppCompatDelegateImpl的setContentView方法:
@Override
public void setContentView(int resId) {ensureSubDecor();ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);contentParent.removeAllViews();LayoutInflater.from(mContext).inflate(resId, contentParent);mOriginalWindowCallback.onContentChanged();
}
经过ensureSubDecor方法后,下面获取到的contentParent已经替换成了刚刚提到的contentView了,接下来通过LayoutInflater.from(mContext).inflate(resId, contentParent)将自己编写的布局加载到这个contentParent中来。
来看看整个流程:

AppCompatActivity布局层级如下:

最后总结一下Activity和AppCompatActivity的setContView方法区别:Activity的setContView直接将我们的布局渲染到mContentParent容器里面AppCompactActivity的setContView会根据不同的主题特性在mContentParent容器里面添加一个不同主题的subDecor容器,在subDecor容器里面有一个id为action_bar_activity_content的ContentFrameLayout容器(后来被替换成了R.id.content),并将我们的布局渲染到ContentFrameLayout里面(ContentFrameLayout也继承自FrameLayout)。
本部分参考:
Android开发Activity的setContentView源码分析_android activity setcontentview 源码-CSDN博客
AppCompatActivity#setContentView源码分析_appcompactactivity decorview-CSDN博客
触控三分显纷争,滑动冲突待分明
接下来进入本文的正文部分(那上面是什么?)
在之前写demo的时候,总能遇到这样的问题:
如图所示,ViewPager2嵌套RecyclerView(由于ViewPager2底层用RecyclerView实现,故而说是双层RecyclerView嵌套也并无不可)。同时,这两层嵌套的View都是水平方向滑动的。
看似是比较常见的布局,但实际运行起来就出现了问题:
当我想滑动内层布局的时候,滑动了外层布局。我如果只想滑动内层布局,就只能用非常缓慢的速度滑动,且滑动非常滞涩。
这是什么原因呢?
上网搜索会发现这样的问题十分经典,叫滑动冲突,初遇时翻看许多相关文档,往往提及事件分发,触摸事件Event,拦截机制等,不解其意。
今天我们就从这里开始,简单说说产生滑动冲突的原因以及应该如何解决滑动冲突。
触控起源定归途
首先我们需要对事件分发机制有一定了解:
我们从手指点击屏幕的那一瞬间开始,当我们点击屏幕时,就产生了点击事件,这个事件被封装成了一个类,MotionEvent。因为当这个 MotionEvent 产生后,系统会将这个 MotionEvent 传递给 View 的层级,MotionEvent 在 View 中的传递过程就是点击事件分发。
我们从三个方法——dispatchTouchEvent、onInterceptTouchEvent 和 onTouchEvent开始介绍事件分发机制。
dispatchTouchEvent(MotionEvent ev) —— dispatchTouchEvent 是事件分发的入口,负责将触摸事件传递给视图的层级。所有的事件传递都会经过这个方法。
onInterceptTouchEvent(MotionEvent ev) —— 用来进行事件的拦截,在 dispatchTouchEvent 方法中调用,需要注意的是 View 没有提供该方法。
onTouchEvent(MotionEvent ev) —— 用来处理点击事件,在 dispatchTouchEvent 方法中进行调用。
放一张流程图在这里:这张图完美诠释了事件分发过程中的向上和向下传递。

事件分发机制
Activity 的 dispatchTouchEvent 方法:
当用户点击屏幕时,系统首先会在
Activity的dispatchTouchEvent方法中处理这个事件。
Activity会将事件交给其内部的PhoneWindow进行处理。具体来说,PhoneWindow是Activity的一个成员,负责处理窗口相关的事件。
PhoneWindow 的事件处理:
PhoneWindow负责将事件传递到DecorView。DecorView是一个包含窗口布局的特殊ViewGroup,它是Activity窗口的根视图,所有 UI 元素都在它的控制下。
PhoneWindow会调用DecorView的dispatchTouchEvent方法,将事件传递给DecorView。
DecorView 事件传递:
DecorView会进一步将事件传递给其子视图。如果DecorView是根视图,通常它的子视图就是整个布局的根ViewGroup。
DecorView通过dispatchTouchEvent将事件传递给这个根ViewGroup,并开始处理事件分发。
我们从ViewGroup的dispatchTouchEvent方法开始分析:
代码如下所示:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {...if ((actionMasked == MotionEvent.ACTION_DOWN) ||cancelAndClearTouchTargets(ev)) {resetTouchState();}if (actionMasked == MotionEvent.ACTION_DOWN) {final boolean isTarget = mFirstTouchTarget != null;final boolean disallowIntercept = (mGroupFlags & FLAGDisallowIntercept) != 0;if (!isTarget || disallowIntercept) {intercepted = onInterceptTouchEvent(ev);if (intercepted) {ev.setAction(action);}} else {intercepted = false;}} else {...}return handled;
}
首先,系统会判断是否需要重置触摸状态或清除目标视图。接着,通过检查是否有触摸目标(mFirstTouchTarget)以及是否允许拦截(disallowIntercept 标志位),决定是否调用 onInterceptTouchEvent 来判断是否拦截当前事件。如果事件被拦截,ev.setAction(action) 会确保事件继续保持正确的状态。如果不拦截,事件会继续传递给子视图,直到被处理或消费。最后,handled 标志返回事件是否已经处理。
mFirstTouchTarget是ViewGroup中的一个内部成员变量,表示当前触摸事件的第一个触摸目标视图。它的主要作用是在事件分发过程中,指示哪个视图正在处理触摸事件。初始触摸目标:在触摸事件开始(
ACTION_DOWN)时,mFirstTouchTarget会被设置为接收到该事件的第一个视图。这个视图会处理接下来的所有触摸事件,直到事件处理完成或被中断。
接下来看看 onInterceptTouchEvent() 方法:
public boolean onInterceptTouchEvent(MotionEvent ev) {return false;
}
onInterceptTouchEvent() 方法默认为 false,不进行拦截。如果想要拦截事件,那么应该在自定义的 ViewGroup 中重写这个方法。
接着来看看 dispatchTouchEvent() 方法剩余的部分源码,如下所示:
public boolean dispatchTouchEvent(MotionEvent event) {boolean result = false;if (onFilterTouchEventForSecurity(event)) {final int action = event.getAction();if (action == MotionEvent.ACTION_DOWN) {result = onTouchEvent(event);} else {result = onTouchEvent(event);}}return result;
}
如果 View 设置了点击事件监听器 OnClickListener,那么它的 onClick 方法就会被执行。
点击事件分发的传递规则
用伪代码来简单表示一下点击事件分发的这 3 个重要方法的关系:
public boolean dispatchTouchEvent(MotionEvent event) {boolean result = false;if (onFilterTouchEventForSecurity(event)) {boolean intercepted = onInterceptTouchEvent(event);if (intercepted || (action == MotionEvent.ACTION_DOWN && mFirstTouchTarget != null)) {result = onTouchEvent(event);} else {for (int i = 0; i < childrenCount; i++) {final int childIndex = getChildIndex();View child = getChildAt(childIndex);if (canViewReceivePointerEvents(child) && isTransformedTouchPointInView(x, y, child, null)) {ev.setTargetAccessibilityFocus(false);newTouchTarget = getTouchTarget(child);if (newTouchTarget != null) {if (newTouchTarget.pointerIdBits != idBitsToAssign) {break;}resetCancelNextUpFlag(child);if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {mLastTouchDownIndex = childIndex;mLastTouchDownX = ev.getX();mLastTouchDownY = ev.getY();newTouchTarget = addTouchTarget(child, idBitsToAssign);alreadyDispatchedNewTouchTarget = true;break;}} else {mLastTouchDownIndex = childIndex;}}}}}ev.setTargetAccessibilityFocus(false);
}
在这段代码中,我们可以看到触摸事件分发的过程,特别是如何判断哪个子视图处理触摸事件。
通过 for 循环遍历 ViewGroup 的子视图时,采用倒序遍历的方式。这意味着,系统首先从最上层的子视图(通常是最上面显示的视图)开始检查,依次检查每个子视图是否能够接收当前的触摸事件。
倒序遍历的原因通常是基于“Z轴顺序”原则,最上层的视图应该优先接收触摸事件。如果最上层的视图无法接收事件,则继续检查其他子视图,直到找到合适的视图来处理该事件。
在判断是否将事件传递给子视图时,代码会检查触摸点是否位于当前子视图的区域内(即是否在该视图的边界范围内)。如果触摸点位于子视图内部,说明该视图可以处理该事件。
另外,代码还会检查该子视图是否正在执行动画。如果子视图正在播放动画,通常会阻止事件的处理,因为动画可能正在改变视图的状态或位置,导致触摸事件的处理不一致或不准确。
如果这两个条件都不满足(即触摸点不在子视图范围内,且子视图不在播放动画),则使用 continue 语句跳过当前子视图,继续遍历下一个子视图。
接下来看 dispatchTransformedTouchEvent() 方法做了什么,代码如下所示:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIndex) {final int oldAction = event.getAction();if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {event.setAction(MotionEvent.ACTION_CANCEL);if (child == null) {handled = super.dispatchTouchEvent(event);} else {handled = child.dispatchTouchEvent(event);}event.setAction(oldAction);return handled;}...
}
这个方法的作用是将事件分发到视图树中的特定视图,并根据需要调整事件的坐标或其他属性(例如,经过变换的坐标或视图的缩放)。
如果有子 View,则调用子 View 的 dispatchTouchEvent() 方法。如果是 ViewGroup 没有子 View,则调用 super.dispatchTouchEvent(event) 方法。ViewGroup 是继承自 View 的。下面再来看看 View 的 dispatchTouchEvent 方法:
public boolean dispatchTouchEvent(MotionEvent event) {boolean result = false;if ((mViewFlags & ENABLED_MASK) == ENABLED&& !(mViewFlags & CLICKABLE || mViewFlags & LONG_CLICKABLE)) {if (!mOnTouchListener.hasListeners()) {result = true;}if (result || onTouchEvent(event)) {result = true;}}...return result;
}
我们看到如果 OnTouchListener 不为 null 并且 onTouch 方法返回 true,则表示事件被消费,就不会执行 onTouchEvent(event),否则就会执行 onTouchEvent(event),可以看出 OnTouchListener 中的 onTouch 方法优先级要高于 onTouchEvent 方法。
下面再来看看 onTouchEvent 方法的部分源码:
public boolean onTouchEvent(MotionEvent event) {final int action = event.getAction();if (!(viewFlags & CLICKABLE || viewFlags & LONG_CLICKABLE || viewFlags & CONTEXT_CLICKABLE)) {return false;}switch (action) {case MotionEvent.ACTION_UP:boolean pressed = (mPrivateFlags & PFLAG_PRESSED) != 0;if ((mPrivateFlags & PFLAG_PRESSED) != 0 || pressed) {boolean focusTaken = false;if (!mHasPerformedLongPress && !ignoreNextUpEvent) {if (mPerformClick != null) {mPerformClick.onPerformClick();}if (post(mPerformClick)) {performClick();}}}break;...}return true;
}
从上面的代码中可以看到,只要 View 的 CLICKABLE 和 LONG_CLICKABLE 有一个为 true,那么 onTouchEvent() 就会返回 true 消耗这个事件。CLICKABLE和 LONG_CLICKABLE 代表 View 可以被点击和长按点击,这可以通过 View 的 setClickable 和 setLongClickable 方法来设置,也可以通过 View 的 XML 文件来设置。接着在 ACTION_UP 事件中会调用 performClick 方法:
public boolean performClick() {final boolean result;final ListenerInfo li = mListenerInfo;if (li != null && li.mOnClickListener != null) {playSoundEffect(SoundEffects.CLICK);li.mOnClickListener.onClick(this);result = true;} else {sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);result = false;}return result;
}
如果 View 设置了点击事件监听器 OnClickListener,那么它的 onClick 方法就会被执行。
产生滑动冲突的原因
了解了滑动冲突的基本原理,我们现在大概清楚了,是由于在Move事件分发的时候,被外层的ViewPager2提前拦截了,而导致RecyclerView无法接收到Move事件,从而也无法处理滑动了。
但既然如此,那么按理来说,只要产生任何滑动都会被ViewPager2拦截,但为什么在缓慢滑动的时候,RecyclerView有时会接收到滑动事件从而进行滑动呢?
我们直接看看RecyclerView的拦截onInterceptTouchEvent方法,一探究竟。
@Overridepublic boolean onInterceptTouchEvent(MotionEvent e) {if (mLayoutSuppressed) {// When layout is suppressed, RV does not intercept the motion event.// A child view e.g. a button may still get the click.return false;}// Clear the active onInterceptTouchListener. None should be set at this time, and if one// is, it's because some other code didn't follow the standard contract.mInterceptingOnItemTouchListener = null;if (findInterceptingOnItemTouchListener(e)) {cancelScroll();return true;}if (mLayout == null) {return false;}final boolean canScrollHorizontally = mLayout.canScrollHorizontally();final boolean canScrollVertically = mLayout.canScrollVertically();if (mVelocityTracker == null) {mVelocityTracker = VelocityTracker.obtain();}mVelocityTracker.addMovement(e);final int action = e.getActionMasked();final int actionIndex = e.getActionIndex();switch (action) {case MotionEvent.ACTION_DOWN:if (mIgnoreMotionEventTillDown) {mIgnoreMotionEventTillDown = false;}mScrollPointerId = e.getPointerId(0);mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);if (stopGlowAnimations(e) || mScrollState == SCROLL_STATE_SETTLING) {getParent().requestDisallowInterceptTouchEvent(true);setScrollState(SCROLL_STATE_DRAGGING);stopNestedScroll(TYPE_NON_TOUCH);}// Clear the nested offsetsmNestedOffsets[0] = mNestedOffsets[1] = 0;int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;if (canScrollHorizontally) {nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;}if (canScrollVertically) {nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;}startNestedScroll(nestedScrollAxis, TYPE_TOUCH);break;case MotionEvent.ACTION_POINTER_DOWN:mScrollPointerId = e.getPointerId(actionIndex);mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);break;case MotionEvent.ACTION_MOVE: {final int index = e.findPointerIndex(mScrollPointerId);if (index < 0) {Log.e(TAG, "Error processing scroll; pointer index for id "+ mScrollPointerId + " not found. Did any MotionEvents get skipped?");return false;}final int x = (int) (e.getX(index) + 0.5f);final int y = (int) (e.getY(index) + 0.5f);if (mScrollState != SCROLL_STATE_DRAGGING) {final int dx = x - mInitialTouchX;final int dy = y - mInitialTouchY;boolean startScroll = false;if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {mLastTouchX = x;startScroll = true;}if (canScrollVertically && Math.abs(dy) > mTouchSlop) {mLastTouchY = y;startScroll = true;}if (startScroll) {setScrollState(SCROLL_STATE_DRAGGING);}}}break;case MotionEvent.ACTION_POINTER_UP: {onPointerUp(e);}break;case MotionEvent.ACTION_UP: {mVelocityTracker.clear();stopNestedScroll(TYPE_TOUCH);}break;case MotionEvent.ACTION_CANCEL: {cancelScroll();}}return mScrollState == SCROLL_STATE_DRAGGING;}
直接截取核心部分:
case MotionEvent.ACTION_MOVE: {final int x = (int) (e.getX(index) + 0.5f);final int y = (int) (e.getY(index) + 0.5f);if (mScrollState != SCROLL_STATE_DRAGGING) {final int dx = x - mInitialTouchX;final int dy = y - mInitialTouchY;boolean startScroll = false;if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {mLastTouchX = x;startScroll = true;}if (canScrollVertically && Math.abs(dy) > mTouchSlop) {mLastTouchY = y;startScroll = true;}if (startScroll) {setScrollState(SCROLL_STATE_DRAGGING);}}}break;
当触摸事件为 ACTION_MOVE 事件类型时,表示用户正在滑动手指。每次用户滑动手指时都会触发这个事件。
通过 e.getX(index) 和 e.getY(index) 获取触摸事件的当前位置。
这里检查当前的滑动状态是否是 拖动中(SCROLL_STATE_DRAGGING)。如果不是拖动状态,才会继续判断是否进入拖动状态。
计算当前触摸点与最初触摸点(mInitialTouchX 和 mInitialTouchY)的水平和垂直偏移量。dx 是水平方向上的偏移量,dy 是竖直方向上的偏移量。这个偏移量表示触摸点相对初始位置的滑动距离。
startScroll 用来标记是否开始滑动,初始设为 false。
如果支持水平滑动(canScrollHorizontally 为 true),并且水平方向的滑动距离 dx 超过了触摸滑动阈值 mTouchSlop,则认为开始了水平滑动。mLastTouchX 记录当前的 x 坐标,表示上次滑动位置,并将 startScroll 设置为 true,表示滑动开始。
同样,如果支持竖直滑动(canScrollVertically 为 true),并且竖直方向的滑动距离 dy 超过了 mTouchSlop,则认为开始了竖直滑动。mLastTouchY 记录当前的 y 坐标,表示上次滑动位置,并将 startScroll 设置为 true,表示滑动开始。
如果 startScroll 为 true,表示触摸滑动已经开始,此时调用 setScrollState(SCROLL_STATE_DRAGGING) 将滚动状态设置为 拖动中(DRAGGING),标志着滑动已经开始,视图可以开始响应滚动。
综上所述,在判断是否拦截此滑动事件时,有一个属性mTouchSlop起到了关键作用。
那么是否是因为RecyclerView和ViewPager2的mTouchSlop的大小设置不同导致的呢?
我们再进入源码当中看一下,搜索mTouchSlop,可以找到这样一个方法。
可以发现RecyclerView有两种TouchSlop,默认的TouchSlop是PagingTouchSlop的两倍。在RecyclerView中,TouchSlop默认是:
而在ViewPager2中,我们找到了这样一句:
破案!滑动滞涩本质上是事件拦截,而如果滑动的比较慢,那么ViewPager2不会将此滑动事件拦截,而是继续传递给内层的RecyclerView,由于内层的TouchSlop比较小,所以将事件拦截下来。
拦截消冲理滑争
现在我们已经了解了事件分发的机制,并且了解了产生滑动冲突的原因,那我们应该如何解决这个问题呢?
网络上常见的解决方案有几种:
禁用父
ViewPager2的滑动:如果嵌套的ViewPager2实例不需要滚动,可以禁用父ViewPager2实例的滑动。这可以通过调用ViewPager2.setUserInputEnabled(false)方法来实现。(那为什么用VP2…?)子view拿到事件后,通过调用requestDisallowInterceptTouchEvent(true)来禁用父view拦截事件(父View不会再调用自己的onInterceptTouchEvent方法了)
学习解决方案之前,还有一件事需要我们了解:DOWN事件不会被拦截。
根据刚刚的学习,我们只了解了什么时候拦截器会拦截MOVE事件,但如果拦截器会将任何正常情况下的MOVE事件拦截,我们如何确保事件能传入下一层View中呢?
我们再回到拦截事件的源码当中:
在这个方法中,判断是否拦截事件的依据是,当前的滑动状态是否为SCROLL_STATE_DRAGGING,那这个状态在哪里会改变呢?
switch (action) {case MotionEvent.ACTION_DOWN:if (mIgnoreMotionEventTillDown) {mIgnoreMotionEventTillDown = false;}mScrollPointerId = e.getPointerId(0);mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);if (stopGlowAnimations(e) || mScrollState == SCROLL_STATE_SETTLING) {getParent().requestDisallowInterceptTouchEvent(true);setScrollState(SCROLL_STATE_DRAGGING);stopNestedScroll(TYPE_NON_TOUCH);}// Clear the nested offsetsmNestedOffsets[0] = mNestedOffsets[1] = 0;int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;if (canScrollHorizontally) {nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;}if (canScrollVertically) {nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;}startNestedScroll(nestedScrollAxis, TYPE_TOUCH);break;case MotionEvent.ACTION_POINTER_DOWN:mScrollPointerId = e.getPointerId(actionIndex);mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);break;case MotionEvent.ACTION_MOVE: {final int index = e.findPointerIndex(mScrollPointerId);if (index < 0) {Log.e(TAG, "Error processing scroll; pointer index for id "+ mScrollPointerId + " not found. Did any MotionEvents get skipped?");return false;}final int x = (int) (e.getX(index) + 0.5f);final int y = (int) (e.getY(index) + 0.5f);if (mScrollState != SCROLL_STATE_DRAGGING) {final int dx = x - mInitialTouchX;final int dy = y - mInitialTouchY;boolean startScroll = false;if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {mLastTouchX = x;startScroll = true;}if (canScrollVertically && Math.abs(dy) > mTouchSlop) {mLastTouchY = y;startScroll = true;}if (startScroll) {setScrollState(SCROLL_STATE_DRAGGING);}}}break;case MotionEvent.ACTION_POINTER_UP: {onPointerUp(e);}break;case MotionEvent.ACTION_UP: {mVelocityTracker.clear();stopNestedScroll(TYPE_TOUCH);}break;case MotionEvent.ACTION_CANCEL: {cancelScroll();}}
我们发现在MotionEvent.ACTION_DOWN这一case中,滑动状态仅有一种可能会改变,即SCROLL_STATE_SETTLING时。而一般情况的点击是不会对这一属性进行更改的,也就是说RecyclerView并没有拦截down事件。
我们来梳理一下大致的流程:
ACTION_DOWN事件的处理触发
ACTION_DOWN事件:触摸事件从用户的手指接触屏幕开始,首先触发ACTION_DOWN事件。此事件首先进入RecyclerView的onInterceptTouchEvent方法。
RecyclerView没有拦截事件:此时RecyclerView的onInterceptTouchEvent返回false,并将事件交给其子项(通常是ItemView)进行处理。
ItemView消费了ACTION_DOWN事件:如果ItemView可以点击,并且它的onTouchEvent方法默认返回true,那么它消费了该事件。此时,RecyclerView依然没有拦截任何事件,mFirstTouchTarget被设置为ItemView,标记着ItemView现在是当前触摸的目标。
ACTION_MOVE事件的处理
ACTION_MOVE到达:用户开始滑动时,ACTION_MOVE事件会继续传递给RecyclerView。此时,mFirstTouchTarget已经指向了ItemView,所以事件会先进入RecyclerView的onInterceptTouchEvent方法。滑动距离超过最小阈值:如果
ACTION_MOVE事件的滑动距离超过了设定的最小滑动阈值(mTouchSlop),RecyclerView会判断此次事件是滑动事件而非点击事件。
RecyclerView返回true:由于滑动事件的发生,RecyclerView会开始拦截事件,返回true,表示它已消费了该事件,阻止进一步的分发。这时,intercepted被设置为true,表示触摸事件已经被拦截。
ACTION_CANCEL事件的触发
cancelChild被设置为true:由于滑动距离超过了阈值并且RecyclerView拦截了事件,cancelChild被设置为true,表示当前的子项(即ItemView)需要取消事件的处理。触发
ACTION_CANCEL:当cancelChild为true时,RecyclerView会调用dispatchTransformedTouchEvent方法,将一个ACTION_CANCEL事件发送给ItemView,通知它取消当前的触摸事件处理。这意味着ItemView会清除所有当前的触摸状态,并停止响应当前的触摸事件。事件传递过程的重置
mFirstTouchTarget被重置为null:在事件流程中的某个时刻,mFirstTouchTarget被清空,意味着RecyclerView不再跟踪ItemView作为当前的触摸目标。后续事件被
RecyclerView消费:由于mFirstTouchTarget为空,后续的触摸事件将直接交由RecyclerView处理,RecyclerView会重新开始处理触摸事件,直到触摸事件处理完毕。
由于RecyclerView并没有拦截down事件,我们可以在接受到down事件的时候请求父View不要拦截事件
后续move事件到我们这并且水平滑动距离大于最小滑动距离的时候,再问我们的子RecyclerVIew能否在这个滑动方向滑动,如果能,继续禁止父类拦截事件。如果不能则允许父类拦截事件(此时可以根据自己的想法来设置)。
EasyTry嵌套,强制拦截
public class easytryView extends LinearLayout {public easytryView(Context context) {super(context);}public easytryView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);}@Overridepublic boolean onInterceptTouchEvent(MotionEvent event) {getParent().requestDisallowInterceptTouchEvent(true);return false;}@Overridepublic boolean onTouchEvent(MotionEvent event) {getParent().requestDisallowInterceptTouchEvent(true);return true;}
}
防止父视图拦截事件:
requestDisallowInterceptTouchEvent(true) 在 onInterceptTouchEvent 和 onTouchEvent 中都调用,告诉父视图不要拦截触摸事件,确保当前的 easytryView 能够完全控制事件的处理。
确保当前视图处理触摸事件:
onInterceptTouchEvent 返回 false 表示 easytryView 不打算拦截事件,而是将事件交给 onTouchEvent 处理。
onTouchEvent 返回 true 表示事件已被消费,不再交给其他视图处理。
NestedScrollableHost嵌套,选择性拦截
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.FrameLayout;import androidx.viewpager2.widget.ViewPager2;public class NestedScrollableHost extends FrameLayout {private int touchSlop;private float initialX = 0f;private float initialY = 0f;// 循环遍历找到ViewPager2private ViewPager2 parentViewPager;public NestedScrollableHost(Context context) {super(context);init(context);}public NestedScrollableHost(Context context, AttributeSet attrs) {super(context, attrs);init(context);}private void init(Context context) {touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();}// 循环遍历找到ViewPager2private ViewPager2 getParentViewPager() {View v = (View) getParent();while (v != null && !(v instanceof ViewPager2)) {v = (View) v.getParent();}return (ViewPager2) v;}// 找到子RecyclerViewprivate View getChildView() {return getChildCount() > 0 ? getChildAt(0) : null;}private boolean canChildScroll(int orientation, float delta) {int direction = (int) (-Math.signum(delta));if (orientation == 0) {return getChildView() != null && getChildView().canScrollHorizontally(direction);} else if (orientation == 1) {return getChildView() != null && getChildView().canScrollVertically(direction);} else {throw new IllegalArgumentException("Invalid orientation");}}@Overridepublic boolean onInterceptTouchEvent(MotionEvent e) {handleInterceptTouchEvent(e);return super.onInterceptTouchEvent(e);}private void handleInterceptTouchEvent(MotionEvent e) {ViewPager2 viewPager2 = getParentViewPager();if (viewPager2 == null) return;int orientation = viewPager2.getOrientation();// 如果子RecyclerView在viewPager2的滑动方向上不能滑动直接返回if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {return;}if (e.getAction() == MotionEvent.ACTION_DOWN) {initialX = e.getX();initialY = e.getY();// down事件直接强制禁止父view拦截事件// 后续事件先交给子RecyclerView判断是否能够消费// 如果这一块不强制禁止父view会导致后续事件可能直接没到子RecyclerView就被父view拦截了getParent().requestDisallowInterceptTouchEvent(true);} else if (e.getAction() == MotionEvent.ACTION_MOVE) {// 计算手指滑动距离float dx = e.getX() - initialX;float dy = e.getY() - initialY;boolean isVpHorizontal = orientation == ViewPager2.ORIENTATION_HORIZONTAL;float scaledDx = Math.abs(dx) * (isVpHorizontal ? 0.5f : 1f);float scaledDy = Math.abs(dy) * (isVpHorizontal ? 1f : 0.5f);// 滑动距离超过最小滑动值if (scaledDx > touchSlop || scaledDy > touchSlop) {if (isVpHorizontal == (scaledDy > scaledDx)) {// 如果viewPager2是横向滑动但手势是竖直方向滑动,则允许所有父类拦截getParent().requestDisallowInterceptTouchEvent(false);} else {// 手势滑动方向和viewPager2是同方向的,需要询问子RecyclerView是否在同方向能滑动if (canChildScroll(orientation, isVpHorizontal ? dx : dy)) {// 子RecyclerView能滑动直接禁止父view拦截事件getParent().requestDisallowInterceptTouchEvent(true);} else {// 子RecyclerView不能滑动(划到第一个Item还往右滑或者划到最后一个Item还往左划的时候)允许父view拦截getParent().requestDisallowInterceptTouchEvent(false);}}}}}
}
本段内容参考:
【View系列】手把手教你解决ViewPager2滑动冲突 - 知乎
稍微解释一下这段代码,判断是否存在的安全性代码,暂且略过,我们且看核心部分的事件处理逻辑。
private void handleInterceptTouchEvent(MotionEvent e) {ViewPager2 viewPager2 = getParentViewPager();if (viewPager2 == null) return;int orientation = viewPager2.getOrientation();// 如果子RecyclerView在viewPager2的滑动方向上不能滑动直接返回if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {return;}if (e.getAction() == MotionEvent.ACTION_DOWN) {initialX = e.getX();initialY = e.getY();// down事件直接强制禁止父view拦截事件// 后续事件先交给子RecyclerView判断是否能够消费// 如果这一块不强制禁止父view会导致后续事件可能直接没到子RecyclerView就被父view拦截了getParent().requestDisallowInterceptTouchEvent(true);} else if (e.getAction() == MotionEvent.ACTION_MOVE) {// 计算手指滑动距离float dx = e.getX() - initialX;float dy = e.getY() - initialY;boolean isVpHorizontal = orientation == ViewPager2.ORIENTATION_HORIZONTAL;float scaledDx = Math.abs(dx) * (isVpHorizontal ? 0.5f : 1f);float scaledDy = Math.abs(dy) * (isVpHorizontal ? 1f : 0.5f);// 滑动距离超过最小滑动值if (scaledDx > touchSlop || scaledDy > touchSlop) {if (isVpHorizontal == (scaledDy > scaledDx)) {// 如果viewPager2是横向滑动但手势是竖直方向滑动,则允许所有父类拦截getParent().requestDisallowInterceptTouchEvent(false);} else {// 手势滑动方向和viewPager2是同方向的,需要询问子RecyclerView是否在同方向能滑动if (canChildScroll(orientation, isVpHorizontal ? dx : dy)) {// 子RecyclerView能滑动直接禁止父view拦截事件getParent().requestDisallowInterceptTouchEvent(true);} else {// 子RecyclerView不能滑动(划到第一个Item还往右滑或者划到最后一个Item还往左划的时候)允许父view拦截getParent().requestDisallowInterceptTouchEvent(false);}}}}}
ACTION_DOWN:记录触摸初始位置,禁止父视图拦截事件(requestDisallowInterceptTouchEvent(true)),确保后续事件能够到达子视图。
ACTION_MOVE:
计算当前手指滑动的距离,并根据滑动方向判断是否允许父视图拦截。
如果手指滑动的方向与 ViewPager2 的滚动方向不一致(例如,ViewPager2 是横向滑动,用户手势是竖直滑动),则允许父视图(ViewPager2)拦截事件。
如果手指滑动的方向与 ViewPager2 一致,则检查子 RecyclerView 是否能够滚动。如果可以滚动,禁止父视图拦截事件,反之,允许父视图拦截。
结语
本篇博客的代码部分仍然存在一些问题,关于Activtiy层级的部分也并没有多做阐释,学习不够深入,日后考虑写几篇博客填充一下这部分内容。
相关文章:
【Android】从事件分发开始:原理解析如何解决滑动冲突
【Android】从事件分发开始:原理解析如何解决滑动冲突 文章目录 【Android】从事件分发开始:原理解析如何解决滑动冲突Activity层级结构浅析Activity的setContentView源码浅析AppCompatActivity的setContentView源码 触控三分显纷争,滑动冲突…...
如何使用JDBC向数据库中插入日期数据???
在学习JDBC 的过程中很多小明有疑问在IDEA编辑器是如何插入一个日期类型的数据的,此篇一些方法希望可以帮助到你。 示例: import java.text.ParseException; import java.text.SimpleDateFormat; import java.sql.Date; import java.util.Scanner;publi…...
高频面试题(含笔试高频算法整理)基本总结回顾29
干货分享,感谢您的阅读! (暂存篇---后续会删除,完整版和持续更新见高频面试题基本总结回顾(含笔试高频算法整理)) 备注:引用请标注出处,同时存在的问题请在相关博客留言…...
Flink日志配置
所有Flink进程都会创建一个日志文本文件,其中包含进程中发生的各种事件的消息。这些日志可以深入了解Flink的内部工作原理,还可以用来检测问题(以警告/错误信息的形式),并帮助调试。 可以通过web界面的JobManager/TaskManager页面访问日志文件。使用的资源提供者(例如YA…...
论文 | EfficientRAG: Efficient Retriever for Multi-Hop Question Answering
1. 论文介绍与研究动机 本文提出了一个新的检索增强生成(RAG)方法——EfficientRAG,它专门用于解决复杂的多跳问题。在多跳问答中,问题的答案需要从多个信息源中检索并结合起来,远比单跳问题复杂,因此也更加…...
超越Hallo和AniPortrait?音频驱动肖像动画新方法LetsTalk
之前的文章中已经给大家介绍过许多关于音频驱动的肖像图像生成动画方法,感兴趣的小伙伴可以点击下面链接阅读~ 复旦开源Hallo:只需输入一段音频和一张照片就可以让人物说话。 开源EMO再升级!复旦|百度|南大推出Hallo2:可以生成4…...
手机LCD分区刷新技术介绍
分区刷新也称为分区变频,LCD分区刷新功能的目的是将屏幕分为上下半区,分区显示不同帧率,上方区块High Frame Rate,下方区块Low Frame Rate。使用者可以动态自定义上方高刷显示区的结尾位置。 当前的智能手机屏幕上,显示…...
WPF软件花屏的解决方法
Win10操作系统更新后,软件花屏了! WPF为啥还能出现花屏呢? 花屏是个什么现象? 即:WPF的界面不能正确渲染或及时刷新,导致整个界面会出现严重的残影,严重影响使用。 如果存在花屏,…...
深度学习笔记——模型压缩和优化技术(蒸馏、剪枝、量化)
本文详细介绍模型训练完成后的压缩和优化技术:蒸馏、剪枝、量化。 文章目录 1. 知识蒸馏 (Knowledge Distillation)基本概念工作流程关键技术类型应用场景优势与挑战优势挑战 总结 2. 权重剪枝 (Model Pruning)基本原理二分类1. 非结构化剪枝(Unstructur…...
开发手札:Win+Mac下工程多开联调
最近完成一个Windows/Android/IOS三端多人网络协同项目V1.0版本,进入测试流程了。为了方便自测,需要用unity将一个工程打开多次,分别是Win/IOS/Android版本,进行多角色联调。 在Win开发机上,以Windows版本为主版…...
项目基于oshi库快速搭建一个cpu监控面板
后端: <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.github.oshi</groupId><artifactId>oshi-…...
【c语言】指针3
1、字符指针变量 指针类型中我们知道有一种为字符指针char*的指针类型,其使用方法如下: 上面我们是先将字符使用一个变量,然后将变量的地址传给一个字符指针变量,通过指针变 量实现了对这个字符的打印。还有下面的这种…...
【开源】A063—基于Spring Boot的农产品直卖平台的设计与实现
🙊作者简介:在校研究生,拥有计算机专业的研究生开发团队,分享技术代码帮助学生学习,独立完成自己的网站项目。 代码可以查看项目链接获取⬇️,记得注明来意哦~🌹 赠送计算机毕业设计600个选题ex…...
Can‘t find variable: token(token is not defined)
文章目录 例子 1:使用 var例子 2:使用 let 或 const例子 3:异步操作你的代码中的情况 Cant find variable: tokentoken is not defined源代码 // index.jsPage({data: {products:[],cardLayout: grid, // 默认卡片布局为网格模式isGrid: tr…...
【JavaEE 初阶】⽹络编程套接字
一、⽹络编程基础 1.应用层 操作系统提供的一组 api >socket api(传输层给应用层提供) 2.传输层 两个核心协议. TCPUDP 差别非常大,编写代码的时候,也是不同的风格 因此, socket api 提供了两套 TCP 有连接, 可靠传输, 面向字节流, 全双工 UDP …...
【Linux内核】Hello word程序
创建测试目录 mkdir -p ~/develop/kernel/hello-1 cd ~/develop/kernel/hello-1 创建MakeFile文件和内核.c文件 nano Makefile nano hello-1.c 编写内容 /* * hello-1.c - The simplest kernel module. */ #include <linux/module.h> /* Needed by all modules */…...
PHP 与 MySQL 搭配的优势
一、PHP 与 MySQL 搭配的优势 强大的动态网页开发能力 PHP 是一种服务器端脚本语言,能够生成动态网页内容。它可以根据用户的请求、数据库中的数据等因素,实时地生成 HTML 页面返回给客户端浏览器。而 MySQL 是一个流行的关系型数据库管理系统…...
深入浅出:PHP中的变量与常量全解析
文章目录 引言理解变量普通变量赋值操作变量间赋值引用赋值取消引用 可变变量预定义变量 理解常量声明常量使用define()函数const关键字 使用常量预定义常量 扩展话题:作用域与生命周期实战案例总结与展望参考资料 引言 在编程的世界里,变量和常量是两种…...
初步简单的理解什么是库,什么是静态库,什么是动态库
库是什么 库根据名字我们应该很容易理解,在我们日常生活种,包含库的东西有很多,像仓库,库房那些,库是拿来存放,方便管理东西的,在我们编程当中,库的定义也是如此 那么为什么要有库…...
从ctfwiki开始的pwn之旅 3.ret2syscall
ret2syscall 原理 ret2syscall,即控制程序执行系统调用,获取 shell。 那么ret2text——程序中有system("/bin/sh")代码段,控制流执行 那么ret2shellcode——程序中不存在system("/bin/sh/")的代码段,自己…...
【网络】每天掌握一个Linux命令 - iftop
在Linux系统中,iftop是网络管理的得力助手,能实时监控网络流量、连接情况等,帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...
TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
阿里云ACP云计算备考笔记 (5)——弹性伸缩
目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...
解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错
出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上,所以报错,到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本,cu、torch、cp 的版本一定要对…...
自然语言处理——Transformer
自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效,它能挖掘数据中的时序信息以及语义信息,但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN,但是…...
MySQL中【正则表达式】用法
MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现(两者等价),用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例: 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...
AI书签管理工具开发全记录(十九):嵌入资源处理
1.前言 📝 在上一篇文章中,我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源,方便后续将资源打包到一个可执行文件中。 2.embed介绍 🎯 Go 1.16 引入了革命性的 embed 包,彻底改变了静态资源管理的…...
Mac下Android Studio扫描根目录卡死问题记录
环境信息 操作系统: macOS 15.5 (Apple M2芯片)Android Studio版本: Meerkat Feature Drop | 2024.3.2 Patch 1 (Build #AI-243.26053.27.2432.13536105, 2025年5月22日构建) 问题现象 在项目开发过程中,提示一个依赖外部头文件的cpp源文件需要同步,点…...
学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”
2025年#高考 将在近日拉开帷幕,#AI 监考一度冲上热搜。当AI深度融入高考,#时间同步 不再是辅助功能,而是决定AI监考系统成败的“生命线”。 AI亮相2025高考,40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕,江西、…...
IP如何挑?2025年海外专线IP如何购买?
你花了时间和预算买了IP,结果IP质量不佳,项目效率低下不说,还可能带来莫名的网络问题,是不是太闹心了?尤其是在面对海外专线IP时,到底怎么才能买到适合自己的呢?所以,挑IP绝对是个技…...
