美文网首页
2018-12-26view 宽高获取

2018-12-26view 宽高获取

作者: 猫KK | 来源:发表于2018-12-26 11:37 被阅读0次

    关于在onCreate或onResume中获取控件的实际宽高

    我们知道在onCreate或onResume 中获取控件的宽高都为0,但是可以通过以下代码来获取

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_animator58);
            mShowBt = findViewById(R.id.show);
            mShowBt.post(new Runnable() {
                @Override
                public void run() {
                    Log.e("TAG", "run: ---->" + mShowBt.getWidth());
                }
            });
            mShowBt.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    mShowBt.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    Log.e("TAG", "onGlobalLayout: ----->" + mShowBt.getWidth());
                }
            });
        }
    
    

    这两个地方都能输出控件的实际宽高。今天就来分析为什么这样可以获取到宽高

    post 的方式

    进入源码

        public boolean post(Runnable action) {
            final AttachInfo attachInfo = mAttachInfo;
            //该值在onCreate 中是 ==null
            if (attachInfo != null) {
                return attachInfo.mHandler.post(action);
            }
    
            // Postpone the runnable until we know on which thread it needs to run.
            // Assume that the runnable will be successfully placed after attach.
            getRunQueue().post(action);
            return true;
        }
    

    首先判断 mAttachInfo 是否为null 可以通过反射来测试在onCreate中为null:

            Class<? extends Button> aClass = mShowBt.getClass();
            try {
                //该属性在view 中,获取button 的父类
                Class<?> superclass = aClass.getSuperclass();
                Field mAttachInfo = superclass.getDeclaredField("mAttachInfo");
                mAttachInfo.setAccessible(true);
                //这里输出null
                Log.e("TAG", "onCreate: ---->" + mAttachInfo.get(mShowBt));
            } catch (Exception e) {
                e.printStackTrace();
            }
    

    通过测试可以知道 mAttachInfo 为null,来看 getRunQueue().post(action);

        private HandlerAction[] mActions;
        private int mCount;
    
        public void post(Runnable action) {
            postDelayed(action, 0);
        }
    
        public void postDelayed(Runnable action, long delayMillis) {
            //HandlerAction 是一个内部类,用于保存action和delayMillis
            final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
    
            synchronized (this) {
                if (mActions == null) {
                    mActions = new HandlerAction[4];
                }
                //将post 中的Runnable 保存到 mActions 数组中
                mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
                mCount++;
            }
        }
    

    根据上面,当调用view.post 方法是,只是将 Runnable 保存到 mActions 数组中,那么这个Runnable中的run 方法什么时候回调呢?
    需要了解,只有当view 执行了 onMeasure 方法设置了自身的大小才能获取到实际的宽高
    由此可以知道在 performTraversals 方法中开始view的onMeasure,来到ViewRootImp 中的performTraversals

    private void performTraversals() {
    //根view DecorView
    final View host = mView;
    //..........省略代码
    
    //为view mAttachInfo 赋值
     mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
    //..........省略代码
    
    //执行保存的Runnable
    getRunQueue().executeActions(mAttachInfo.mHandler);
    //..........省略代码
    
    //开始测量,最终会调用 onMeasure 并设置宽高
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    //..........省略代码
    
    //执行layout
    performLayout(lp, mWidth, mHeight);
    //..........省略代码
    
    //执行draw
    performDraw();
    }
    
    //getRunQueue().executeActions(mAttachInfo.mHandler);
        public void executeActions(Handler handler) {
            synchronized (this) {
                final HandlerAction[] actions = mActions;
                //循环执行保存在mActions 中的 Runnable 就能回调到 onCreate 中的run方法
                for (int i = 0, count = mCount; i < count; i++) {
                    final HandlerAction handlerAction = actions[i];
                    handler.postDelayed(handlerAction.action, handlerAction.delay);
                }
    
                mActions = null;
                mCount = 0;
            }
        }
    

    为什么getRunQueue().executeActions(mAttachInfo.mHandler); 在performMeasure 方法前调用还能获取到宽高呢?
    来看 performTraversals 方法调用的时机

    //ViewRootImpl 中的 scheduleTraversals 方法,具体可见我上一篇内容
    void scheduleTraversals() {
    //是通过handler 的方式来执行
    mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    }
    

    performTraversals 方法是通过handler 的方式来执行 getRunQueue().executeActions(mAttachInfo.mHandler);也是
    通过handler 的方式来执行,通过handler 的源码不难知道getRunQueue().executeActions(mAttachInfo.mHandler) 中
    的handler 会在performTraversals 方法执行完成后执行,类似如下代码

    mHandler.post(new Runnable() {
                @Override
                public void run() {
                    Log.e("TAG", "run: 1----->第一个handler 开始执行");
                    sHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            Log.e("TAG", "run: 3----->第二个handler 开始执行");
                        }
                    });
                    Log.e("TAG", "run: 2----->第一个handler 执行完成");
                }
            });
    

    一个handler 的post 中执行另一个handler 的post 执行顺序为 1 -> 2 -> 3
    所以getRunQueue().executeActions(mAttachInfo.mHandler); 后执行
    post 总结:在onCreate 中调用view.post 时,会将当前的 Runnable 保存,当view 执行了onMeasure 得到实际的宽高后会再执行保存的Runnable

    ViewTreeObserver() 方式

    根据名字大概可以才出来是类似观察这模式,当控件宽高设置后回调。来看源码

    public ViewTreeObserver getViewTreeObserver() {
            //通过上面分析可知 mAttachInfo == null
            if (mAttachInfo != null) {
                return mAttachInfo.mTreeObserver;
            }
             //为 null 则直接 new ViewTreeObserver 返回
            if (mFloatingTreeObserver == null) {
                mFloatingTreeObserver = new ViewTreeObserver(mContext);
            }
            return mFloatingTreeObserver;
        }
    

    其中 ViewTreeObserver 包含各类回调接口和赋值方法,有兴趣可以自行了解

    public void addOnGlobalLayoutListener(OnGlobalLayoutListener listener) {
            //判断是否可用,不可用会直接抛出异常
            checkIsAlive();
            if (mOnGlobalLayoutListeners == null) {
                //CopyOnWriteArray 是内部的一个类似List 的集合
                mOnGlobalLayoutListeners = new CopyOnWriteArray<OnGlobalLayoutListener>();
            }
            //将listener 添加到mOnGlobalLayoutListeners 中
            mOnGlobalLayoutListeners.add(listener);
        }
    

    执行完上面的方法只是在 mOnGlobalLayoutListeners 添加了一个listener 什么时候回调呢?
    来跟踪 mFloatingTreeObserver 看哪里还有使用,发现只有一处地方有使用

    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
            //将 mAttachInfo 赋值
            mAttachInfo = info;
            if (mOverlay != null) {
                mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
            }
            mWindowAttachCount++;
            // We will need to evaluate the drawable state at least once.
            mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
            if (mFloatingTreeObserver != null) {
                //将mFloatingTreeObserver 赋值给info.mTreeObserver
                info.mTreeObserver.merge(mFloatingTreeObserver);
                mFloatingTreeObserver = null;
            }
    }
    
    //ViewTreeObserver 中的 merge 方法
    void merge(ViewTreeObserver observer) {
            //将传进来的 observer 赋值到当前的observer
        if (observer.mOnGlobalLayoutListeners != null) {
                    if (mOnGlobalLayoutListeners != null) {
                        mOnGlobalLayoutListeners.addAll(observer.mOnGlobalLayoutListeners);
                    } else {
                        mOnGlobalLayoutListeners = observer.mOnGlobalLayoutListeners;
                    }
              }
    }
    

    在哪调用dispatchAttachedToWindow 方法呢?通过上面分析 performTraversals 方法可以知道

    private void performTraversals() {
        //该处就会调用view 中的dispatchAttachedToWindow 方法
        host.dispatchAttachedToWindow(mAttachInfo, 0);
    
        performLayout(lp, mWidth, mHeight);
        
        //在 layout 方法调用完成后会调用
        if (triggerGlobalLayoutListener) {
                mAttachInfo.mRecomputeGlobalAttributes = false;
                mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
            }
    }
    
    //ViewTreeObser 中的dispatchOnGlobalLayout 
    public final void dispatchOnGlobalLayout() {
            // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
            // perform the dispatching. The iterator is a safe guard against listeners that
            // could mutate the list by calling the various add/remove methods. This prevents
            // the array from being modified while we iterate it.
            final CopyOnWriteArray<OnGlobalLayoutListener> listeners = mOnGlobalLayoutListeners;
            if (listeners != null && listeners.size() > 0) {
                CopyOnWriteArray.Access<OnGlobalLayoutListener> access = listeners.start();
                try {
                    int count = access.size();
                    for (int i = 0; i < count; i++) {
                        //循环获取lister 调用onGlobalLayout 方法
                        access.get(i).onGlobalLayout();
                    }
                } finally {
                    listeners.end();
                }
            }
        }
    

    ViewTreeObserver() 方式 总结:在onCreate 方法中调用的时候 会创建 一个 ViewTreeObser mFloatingTreeObserver ;之后在performTraversals 方法中会调用dispatchAttachedToWindow 方法将 mFloatingTreeObserver 赋值到mAttachInfo.mTreeObserver 中,在performLayout 执行完毕会执行mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();回调到onGlobalLayout 中,这时就可以获取到控件的实际宽高

    相关文章

      网友评论

          本文标题:2018-12-26view 宽高获取

          本文链接:https://www.haomeiwen.com/subject/rsaclqtx.html