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

相关推荐
私人珍藏库1 小时前
[Android] APK提取器(1.3.7)版本
android
m0_748232641 小时前
mysql的主从配置
android·mysql·adb
秋长愁1 小时前
Android监听应用前台的实现方案解析
android
胖虎12 小时前
2025 新版Android Studio创建Java语言项目
android·java·android studio·创建java项目
JabamiLight3 小时前
Lineageos 22.1(Android 15)Launcer简单调整初始化配置
android·android 15·lineageos 22.1·launcer
敲代码的鱼哇5 小时前
设备唯一ID获取,支持安卓/iOS/鸿蒙Next(uni-device-id)UTS插件
android·ios·uniapp·harmonyos
太空漫步116 小时前
android滑动看新闻
android
KdanMin7 小时前
“让App玩捉迷藏:Android教育平板的‘隐身术’开发实录”
android
韩曙亮7 小时前
【Android】Android 悬浮窗开发 ( 动态权限请求 | 前台服务和通知 | 悬浮窗创建 )
android·service·悬浮窗·服务·前台服务·动态权限·通知通道
双鱼大猫7 小时前
一句话说透Android里面的BufferQueue机制
android·程序员