View的这些基础知识你都掌握了吗?

1.performTraversals()的调用链

  1. ActivityThread 的handleResumeActivity() 里调用 WindowManagerImpl的addView(decor, l)方法,然后调用到WindowManagerGlobal的addView方法
  2. WindowManagerGlobal的addView方法中创建ViewRootImpl,并调用root.setView(view, wparams, panelParentView);
  • View.AttachInfo是在ViewRootImpl的构造方法中创建的
  1. ViewRootImpl 的 setView()方法里会调用requestLayout(),然后调用scheduleTraversals() -> doTraversal() -> performTraversals()
scss 复制代码
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
    
    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
                        
    private void performTraversals() {
        final View host = mView;
        ...
        if (mFirst) {
            host.dispatchAttachedToWindow(mAttachInfo, 0);
        } else {
            ...
        }
        mFirst = false;
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        performLayout(lp, mWidth, mHeight);
        performDraw();
    }

2.view/activity的生命周期关系

在MyActivity中布局添加一个CustomView,然后在各个回调中添加log,会有如下调用顺序:

yaml 复制代码
E/MyActivity: onCreate: 
E/CustomView: onFinishInflate: 
E/MyActivity: onResume: 
E/MyActivity: onAttachedToWindow: 
E/CustomView: onAttachedToWindow: 
E/CustomView: onMeasure: 
E/CustomView: onMeasure: 
E/CustomView: onMeasure: 
E/CustomView: onSizeChanged: 
E/CustomView: onLayout: 
E/CustomView: onDraw: 
E/MainActivity: onWindowFocusChanged: true
E/CustomView: onWindowFocusChanged: true
 
//接下来退出Activity
E/MainActivity: onWindowFocusChanged: false
E/CustomView: onWindowFocusChanged: false
E/MyActivity: onDestroy: 
E/CustomView: onDetachedFromWindow: 
E/MyActivity: onDetachedFromWindow: 

3.自定义view

1. MeasureSpec

模式 意义 对应
EXACTLY 精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Size match_parent
AT_MOST 最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值 wrap_content
UNSPECIFIED 无限制,View对尺寸没有任何限制,View设置为多大就应当为多大 一般系统内部使用

对于DecorView而言,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同决定;对于普通的View,它的MeasureSpec由父视图的MeasureSpec和其自身的LayoutParams共同决定。

2.直接继承View

需要重写onMeasure方法,因为View中并没有对AT_MOST和EXACTLY两个模式做出区分,也就是说View在wrap_content和match_parent两个模式下是完全相同的,都会是match_parent

arduino 复制代码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

重写onMeasure方法

ini 复制代码
/**
 * 重写onMeasure方法
 *
 * @param widthMeasureSpec
 * @param heightMeasureSpec
 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    //处理wrap_contentde情况
    if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, 300);
    } else if (widthMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, heightSize);
    } else if (heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSize, 300);
    }
}

4. 判断View在屏幕中是否可见并获取可见区域

1. View.getVisibility()

只能获取当前View的visibility的值,不受他的父View的visibility的影响(如果view的visibility为true,即使父view的View的visibility为false,该方法仍返回true,其实这个View对用户是不可见的)

2. View.isShown()

java 复制代码
    /**
     * Returns the visibility of this view and all of its ancestors
     *
     * @return True if this view and all of its ancestors are {@link #VISIBLE}
     */
    public boolean isShown() {
        View current = this;
        //noinspection ConstantConditions
        do {
            if ((current.mViewFlags & VISIBILITY_MASK) != VISIBLE) {
                return false;
            }
            ViewParent parent = current.mParent;
            if (parent == null) {
                return false; // We are not attached to the view root
            }
            if (!(parent instanceof View)) {
                return true;
            }
            current = (View) parent;
        } while (current != null);

        return false;
    }
  • 这个方法递归地去检查这个View以及它的parentView的Visibility属性是不是等于View.VISIBLE
  • 另外这个方法还在递归的检查过程中,检查了parentView == null,也就是说所有的parentView都不能为null,否则就说明这个View根本没有被addView过(比如使用Java代码创建界面UI时,可能会先new一个View,然后根据条件动态地把它add到一个ViewGroup中),那肯定是不可能对用户可见的。

3. View.getGlobalVisibleRect

ini 复制代码
Rect rect = new Rect();
boolean visibility = bottom.getGlobalVisibleRect(rect);
  • 当这个View只要有一部分仍然在屏幕中(没有被父View遮挡),那么将把没有被遮挡的那部分区域保存在rect对象中返回,且方法的返回值是true,即visibility=true。此时的rect是以手机屏幕作为坐标系,即==原点是屏幕左上角==;如果它全部被父View遮挡住了或者本身就是不可见的,返回的visibility就为false。
  • 这个方法只能检查出这个View在手机屏幕(或者说是相对它的父View)的位置,而不能检查出与其他兄弟View的相对位置。如果两个View是平级关系,其中一个View遮挡了另一个View,那么被遮挡的View调用该方法返回值仍未true,rect对象的返回值也不受遮挡影响。

4. View.getLocalVisibleRect

  • 这个方法跟getGlobalVisibleRect(rect)唯一的区别就是:getLocalVisibleRect(rect)获得的rect坐标系的==原点是View自己的左上角==,而不是屏幕左上角。所以只要这个View的左上角在屏幕中,它的LocalVisibleRect的左上角坐标就一定是(0,0),如果View的右下角在屏幕中,它的LocalVisibleRect右下角坐标就一定是(view.getWidth(), view.getHeight())。

5. 判断手机屏幕是否熄灭or是否解锁

检查View的可见性虽然和屏幕的状态看起来没有直接关系,但是在做检查前先对屏幕的状态做一个检查也是很有必要的,如果屏幕都已经关闭了,那这个View当然是对用户不可见的。

5. View的坐标

1. View的位置描述

arduino 复制代码
view.width              // View的宽度
view.height             // View的高度

view.x                  // View左上角相对于ViewGroup左上角的X坐标位置
view.y                  // View左上角相对于ViewGroup左上角的Y坐标位置
view.z                  // 相对于屏幕的Z位置

view.left               // View左边相对于ViewGroup左边的距离
view.top                // View上边相对于VIewGroup上边的距离
view.right              // View右边相对于ViewGroup左边的距离
view.bottom             // View底边相对于ViewGroup上边的距离
view.elevation          // View的高度

view.translationX       // 在滑动过程中,View当前位置的最左边和这个View原始位置的最左边的距离
view.translationY       // 在滑动过程中,View当前位置的最上边和这个View原始位置的最上边的距离
view.translationZ       // 在动画过程中,View当前位置的Z轴高度和这个View原始Z轴高度的距离

view进行动画时的计算公式:

ini 复制代码
x = left + translationX;
y = top + translationY;
z = elevation + translationZ;

2. 触摸点的位置描述

csharp 复制代码
event.x         // 触摸点相对于View的X坐标位置
event.y         // 触摸点相对于View的Y坐标位置
event.rawX      // 触摸点相对于屏幕最左侧的距离
event.rawY      // 触摸点相对于屏幕最上侧的距离

3. View相对屏幕的距离

下面介绍三种获取View距离屏幕距离的方法,具体实现方法如下:

arduino 复制代码
// getLocationInWindow
val position = IntArray(2)
viewGroup.getLocationInWindow(position)
Log.d("coordinate-screen", "x: " + position[0])
Log.d("coordinate-screen", "y: " + position[1])

// getLocationOnScreen
val position = IntArray(2)
viewGroup.getLocationOnScreen(position)
Log.d("coordinate-screen", "x: " + position[0])
Log.d("coordinate-screen", "y: " + position[1])

// getGlobalVisibleRect
val rect = Rect()
viewGroup.getGlobalVisibleRect(rect)
Log.d("coordinate-screen", "x: " + rect.left.toString())
Log.d("coordinate-screen", "y: " + rect.top.toString())

Android的自定义View-基础知识-坐标系

6. View的getWidth()和getMeasuredWidth()有什么区别?

  • getMeasuredWidth()和getWidth()分别对应于视图绘制的measure和layout阶段。
  • getMeasuredWidth()获取的是View原始的大小,也就是这个View在XML文件中配置或者是代码中设置的大小;getMeasuredWidth()的值是 ==measure阶段结束之后==得到的View的原始的值
  • getWidth()获取的是这个View最终显示的大小,这个大小有可能等于原始的大小,也有可能不相等;在==layout结束后调用==getWidth()才能获取到View的宽度
  • 在父布局的onLayout()方法或者该View的onDraw()方法里调用measure(0, 0),二者的结果可能会不同

7. RelativeLayout会对子View做两次measure。这是为什么呢?

RelativeLayout中子View的排列方式是基于彼此的依赖关系,而这个依赖关系可能和布局中View的顺序并不相同,在确定每个子View的位置的时候,就需要先给所有的子View排序一下。又因为RelativeLayout允许A、B2个子View,横向上B依赖A,纵向上A依赖B。所以需要横向纵向分别进行一次排序测量。

  • RelativeLayout会让子View调用2次onMeasure,LinearLayout 在有weight时,也会调用子View2次onMeasure
  • RelativeLayout的子View如果高度和RelativeLayout不同,则会引发效率问题,当子View很复杂时,这个问题会更加严重。如果可以,尽量使用padding代替margin。
  • 在不影响层级深度的情况下,使用LinearLayout和FrameLayout而不是RelativeLayout。

RelativeLayout和LinearLayout性能分析

8.Choreographer

Choreographer 的作用主要是配合 Vsync ,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机。目前大部分手机都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每个 16.6 ms , Vsync 信号唤醒 Choreographer 来做 App 的绘制操作,这就是引入 Choreographer 的主要作用。

Choreographer 的作用 Choreographer原理(含native层)

屏幕刷新机制

9.View状态保存与恢复

1. 触发条件
  • 触发Activity 的View 状态保存和恢复只有一种情形:异常销毁。
  • 触发Fragment 的View 状态保存和恢复的两种情形:
    • 异常销毁
    • 事务进出返回栈
2. View状态的保存

无论是对Activity还是Fragment中View状态的保存,本质上都是通过调用Activity /Fragment 中顶级View的saveHierarchyState() 从而实现对Activity /Fragment 中View tree的深度遍历并调用每个View 的onSaveInstanceState() 保存View状态。 除了Fragment 的View状态是保存在Fragment 的一个Field 中外,Activity /Fragment 状态和View状态最终都保存在Activity的一个Field 中。

  1. 异常销毁:Activity的onSaveInstanceState() 中会调用:
  • Activity的顶级View的saveHierarchyState()。
  • Activity中所有activity fragment 的onSaveInstanceState() 和它们的顶级View 的saveHierarchyState()。 注意:Fragment 中顶级View 的saveHierarchyState()不是在它的onSaveInstanceState() (是空实现)中被调用的,这与Activity不同。
  1. 事务进栈:事务通过addToBackStack() 添加到返回栈中会导致所有从前台进入返回栈的Fragment 的顶级View的saveHierarchyState() 被调用。注意:不会触发Activity 和Fragment 的onSaveInstanceState()。
3.View状态的恢复

无论是对Activity还是Fragment中View状态的保存,本质上都是通过调用Activity /Fragment 中顶级View的restoreHierarchyState() 从而实现对Activity /Fragment 中View tree的深度遍历并调用每个View的onRestoreInstanceState() 恢复View状态。

1、异常销毁重建:会触发Activity 和所有active fragment 顶级View的restoreHierarchyState() 。

2、事务出栈:事务出栈会触发该事务涉及到的所有从返回栈出栈转为前台的Fragment 的顶级View的restoreHierarchyState()。

4.总结
  • 实现View状态的自动保存,需要为每个View 设置id。TextView 还需要设置android:freezeText="true"
  • 自定义View要实现自身状态的保存和恢复,需要重写onSaveInstanceState() 和onRestoreInstanceState()

参考资料:

Activity 和Fragment 的异常销毁、状态保存和恢复机制

Android屏幕刷新机制---VSync、Choreographer 全面理解!

深入解析控件测量onMeasure

相关推荐
SRC_BLUE_171 小时前
SQLI LABS | Less-39 GET-Stacked Query Injection-Intiger Based
android·网络安全·adb·less
无尽的大道4 小时前
Android打包流程图
android
镭封6 小时前
android studio 配置过程
android·ide·android studio
夜雨星辰4876 小时前
Android Studio 学习——整体框架和概念
android·学习·android studio
邹阿涛涛涛涛涛涛6 小时前
月之暗面招 Android 开发,大家快来投简历呀
android·人工智能·aigc
IAM四十二6 小时前
Jetpack Compose State 你用对了吗?
android·android jetpack·composer
奶茶喵喵叫7 小时前
Android开发中的隐藏控件技巧
android
Winston Wood8 小时前
Android中Activity启动的模式
android
众乐认证8 小时前
Android Auto 不再用于旧手机
android·google·智能手机·android auto
三杯温开水9 小时前
新的服务器Centos7.6 安卓基础的环境配置(新服务器可直接粘贴使用配置)
android·运维·服务器