各位大佬,能不能随便给我的项目或者之前的文章点个star,苦兮兮。github.com/ 掘金文章
面试官:老哥那么我们继续探讨下这个问题啊。Scrollview和NestScrollView怎么监控呢。
我:???黑人老哥又特么来了。
分析问题
还是和上篇文章一样,我们先看下要解决哪些问题。
- ScrollView NestScrollView 的滑动监控怎么做。
- View有没有像RecyclerView一样的attach和detch方法,超过1.5s的曝光时间。
- View出现一半。
滑动监控
一般人肯定告诉你,这个你自定义个scrollview,然后在onScrollChanged实现个滑动监听的回调什么的。不好意思,我偏不,带你看看另外一个神奇的方法。
先给大家介绍下ViewTreeObserver里面所包含的一些接口。
内部类接口 | 备注 |
---|---|
ViewTreeObserver.OnPreDrawListener | 当视图树将要被绘制时,会调用的接口 |
ViewTreeObserver.OnGlobalLayoutListener | 当视图树的布局发生改变或者View在视图树的可见状态发生改变时会调用的接口 |
ViewTreeObserver.OnGlobalFocusChangeListener | 当一个视图树的焦点状态改变时,会调用的接口 |
ViewTreeObserver.OnScrollChangedListener | 当视图树的一些组件发生滚动时会调用的接口 |
ViewTreeObserver.OnTouchModeChangeListener | 当视图树的触摸模式发生改变时,会调用的接口格 |
各位老哥有没有发现一些奇怪的东西混在里面,哈哈哈。
惯例分析下源码
理论上来说,所有视图状态之类的都是和ViewRootImp相关的。特别是ViewTreeObserver相关的,所以我们的源码分析也是从ViewRootImp开始的。
代码语言:javascript复制class ViewRootImp {
// 根视图绘制
private boolean draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
if (!surface.isValid()) {
return false;
}
if (DEBUG_FPS) {
trackFPS();
}
if (!sFirstDrawComplete) {
synchronized (sFirstDrawHandlers) {
sFirstDrawComplete = true;
final int count = sFirstDrawHandlers.size();
for (int i = 0; i< count; i ) {
mHandler.post(sFirstDrawHandlers.get(i));
}
}
}
scrollToRectOrFocus(null, false);
if (mAttachInfo.mViewScrollChanged) {
mAttachInfo.mViewScrollChanged = false;
// 调用viewtree的滑动监听
mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
}
.....
return useAsyncReport;
}
}
复制代码
上面的代码可以看出,当mAttachInfo.mViewScrollChanged这个状态位被设置成true的情况下,就会通知viewTree调用滑动监听了。 那么我们的切入点就很简单了,什么时候谁把这个值设置成ture了,是不是就会触发滑动监听了呢。
代码语言:javascript复制class View {
final static class AttachInfo {
/**
* Set to true if a view has been scrolled.
*/
@UnsupportedAppUsage
boolean mViewScrollChanged;
}
/**
* This is called in response to an internal scroll in this view (i.e., the
* view scrolled its own contents). This is typically as a result of
* {@link #scrollBy(int, int)} or {@link #scrollTo(int, int)} having been
* called.
*
* @param l Current horizontal scroll origin.
* @param t Current vertical scroll origin.
* @param oldl Previous horizontal scroll origin.
* @param oldt Previous vertical scroll origin.
*/
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
notifySubtreeAccessibilityStateChangedIfNeeded();
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
postSendViewScrolledAccessibilityEventCallback(l - oldl, t - oldt);
}
mBackgroundSizeChanged = true;
mDefaultFocusHighlightSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}
final AttachInfo ai = mAttachInfo;
if (ai != null) {
ai.mViewScrollChanged = true;
}
if (mListenerInfo != null && mListenerInfo.mOnScrollChangeListener != null) {
mListenerInfo.mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt);
}
}
}
复制代码
View.AttachInfo是View的内部类,其注释已经描述了,当view滑动的时候把这个值设置成true。onScrollChanged也是View的protected的方法,而当ScrollView和NestScrollView的滑动状态被改变的时候就会调用这个方法,而这个方法内则就会把状态设置成true。
测试结果
经过在下的测试吧,OnScrollChangedListener在ScrollView和NestScrollView滑动的时候都会触发回调哦。而上述代码分析,则可以说明当两个滑动组件滑动的时候就会触发对应的回调监听。
View 出现一半
这个监控方法还是和上篇文章一样,请各位大佬直接看上篇文章就好了。
1.5s的曝光时长
先回到之前的文章提到onAttachedToWindow
onDetachedFromWindow
的两个方法,这两个可以用吗?答案肯定是不行的。那么我们应该怎么办呢??
没有枪没有炮,还是自己造吧。
代码语言:javascript复制interface ExposeViewAdapter {
fun setExposeListener(listener: (Float) -> Unit)
fun setExposeListener(listener: OnExposeListener)
fun onVisibleChange(isCover: Boolean)
}
复制代码
首先我们可以先提供一个适配器,提供onVisibleChange
这个方法来代替onAttachedToWindow
onDetachedFromWindow
。
class ExposeScrollChangeListener(scrollView: ViewGroup) :
ViewTreeObserver.OnScrollChangedListener, ViewTreeObserver.OnGlobalLayoutListener {
private val rootView: ViewGroup? = scrollView.getChildAt(0) as ViewGroup?
private val views = hashSetOf()
private var lastChildCount = 0
init {
}
override fun onScrollChanged() {
views.forEach {
val exposeView = it as ExposeViewAdapter
exposeView.onVisibleChange(it.visibleRect())
}
}
private fun checkViewSize() {
rootView?.apply {
lastChildCount = childCount
getChildExpose(rootView)
}
}
private fun getChildExpose(view: View?) {
view?.let {
if (it is ExposeViewAdapter) {
views.add(it)
}
if (view is ViewGroup) {
//遍历ViewGroup,是子view加1,是ViewGroup递归调用
for (i in 0 until view.childCount) {
val child = view.getChildAt(i)
if (child is ExposeViewAdapter) {
views.add(child)
}
if (child is ViewGroup) {
getChildExpose(child)
}
}
}
}
}
override fun onGlobalLayout() {
val timeUsage = System.currentTimeMillis()
checkViewSize()
Log.i("expose", "timeCoast:${System.currentTimeMillis() - timeUsage}")
}
}
复制代码
首先我们需要监控onGlobalLayout这个方法,在这个方法触发的情况下,去扫描当前的ViewTree,去获取实现了ExposeViewAdapter的所有的View。当滑动监听触发的时候调用之前的view是否被遮挡的方法来判断当前的view是不是在视图上出现了,然后调用onVisibleChange
来通知视图是否已经从window上移除。
最后
面试官:哎哟不错哟。
我:谦虚有理的小菜逼。
面试官:这种方式感觉还是不够智能,如果让你用动态插桩呢。
我:打扰了,二营长,把我的意大利炮抬过来。
面试官:回家继续等通知把。