View的有效曝光監控(下)|ScrollView NestScrollView篇

面試官:老哥那麼咱們繼續探討下這個問題啊。Scrollview和NestScrollView怎麼監控呢。java

我:???黑人老哥又特麼來了。面試

分析問題

仍是和上篇文章同樣,咱們先看下要解決哪些問題。app

  1. ScrollView NestScrollView 的滑動監控怎麼作。
  2. View有沒有像RecyclerView同樣的attach和detch方法,超過1.5s的曝光時間。
  3. View出現一半。

滑動監控

通常人確定告訴你,這個你自定義個scrollview,而後在onScrollChanged實現個滑動監聽的回調什麼的。很差意思,我偏不,帶你看看另一個神奇的方法。ide

先給你們介紹下ViewTreeObserver裏面所包含的一些接口。源碼分析

內部類接口 備註
ViewTreeObserver.OnPreDrawListener 當視圖樹將要被繪製時,會調用的接口
ViewTreeObserver.OnGlobalLayoutListener 當視圖樹的佈局發生改變或者View在視圖樹的可見狀態發生改變時會調用的接口
ViewTreeObserver.OnGlobalFocusChangeListener 當一個視圖樹的焦點狀態改變時,會調用的接口
ViewTreeObserver.OnScrollChangedListener 當視圖樹的一些組件發生滾動時會調用的接口
ViewTreeObserver.OnTouchModeChangeListener 當視圖樹的觸摸模式發生改變時,會調用的接口格

各位老哥有沒有發現一些奇怪的東西混在裏面,哈哈哈。佈局

慣例分析下源碼

理論上來講,全部視圖狀態之類的都是和ViewRootImp相關的。特別是ViewTreeObserver相關的,因此咱們的源碼分析也是從ViewRootImp開始的。post

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了,是否是就會觸發滑動監聽了呢。測試

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。ui

測試結果

通過在下的測試吧,OnScrollChangedListener在ScrollView和NestScrollView滑動的時候都會觸發回調哦。而上述代碼分析,則能夠說明當兩個滑動組件滑動的時候就會觸發對應的回調監聽。this

View 出現一半

這個監控方法仍是和上篇文章同樣,請各位大佬直接看上篇文章就行了。

1.5s的曝光時長

先回到以前的文章提到onAttachedToWindow onDetachedFromWindow的兩個方法,這兩個能夠用嗎?答案確定是不行的。那麼咱們應該怎麼辦呢??

沒有槍沒有炮,仍是本身造吧。

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<View>()
    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上移除。

最後

面試官:哎喲不錯喲。

我:謙虛有理的小菜逼。

面試官:這種方式感受仍是不夠智能,若是讓你用動態插樁呢。

我:打擾了,二營長,把個人意大利炮擡過來。

面試官:回家繼續等通知把。

相關文章
相關標籤/搜索