android安卓-view.layout()过程详解,为什么view.layout()刷新控件位置

本文深入探讨了Android中View的onMeasure和onLayout过程。layout主要负责确定View在屏幕上的位置,而onLayout在ViewGroup中是抽象的,需要子类重写以布置子视图。measure过程计算出的mMeasuredWidth和mMeasuredHeight为layout提供参考,但非必需。开发者可以根据需求直接在layout中指定视图尺寸。文章还提及了getWidth()与getMeasuredWidth()的区别,并指出measure和layout的关系以及在自定义ViewGroup时的注意事项。

原帖: link

相比较onMeasure
,layout过程要简单多了,正如layout的中文意思“布局”中表达的一样,layout的过程就是确定View在屏幕上显示的具体位置,在代码中就是设置其成员变量mLeft,mTop,mRight,mBottom的值,这几个值构成的矩形区域就是该View显示的位置,不过这里的具体位置都是相对与父视图的位置。

与onMeasure过程类似,ViewGroup在onLayout函数中通过调用其children的layout函数来设置子视图相对与父视图中的位置,具体位置由函数layout的参数决定,当我们继承ViewGroup时必须重载onLayout函数(ViewGroup中onLayout是abstract修饰),然而onMeasure并不要求必须重载,因为相对与layout来说,measure过程并不是必须的,具体后面会提到。首先我们来看下View.java中函数layout和onLayout的源码:

public void layout(int l, int t, int r, int b) {
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        boolean changed = setFrame(l, t, r, b);
        if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
            if (ViewDebug.TRACE_HIERARCHY) {
                ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
            }

            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }
        mPrivateFlags &= ~FORCE_LAYOUT;
    }

函数layout的主体过程还是很容易理解的,首先通过调用setFrame函数来对4个成员变量(mLeft,mTop,mRight,mBottom)赋值,然后回调onLayout函数,最后回调所有注册过的listener的onLayoutChange函数。

对于View来说,onLayout只是一个空实现,一般情况下我们也不需要重载该函数:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

接着我们来看下ViewGroup.java中layout的源码:

public final void layout(int l, int t, int r, int b) {
        if (mTransition == null || !mTransition.isChangingLayout()) {
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutSuppressed = true;
        }
    }

super.layout(l, t, r, b)调用的即是View.java中的layout函数,相比之下ViewGroup增加了LayoutTransition的处理,LayoutTransition是用于处理ViewGroup增加和删除子视图的动画效果,也就是说如果当前ViewGroup未添加LayoutTransition动画,或者LayoutTransition动画此刻并未运行,那么调用super.layout(l, t, r, b),继而调用到ViewGroup中的onLayout,否则将mLayoutSuppressed设置为true,等待动画完成时再调用requestLayout()。
上面super.layout(l, t, r, b)会调用到ViewGroup.java中onLayout,其源码实现如下:

  @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

和前面View.java中的onLayout实现相比,唯一的差别就是ViewGroup中多了关键字abstract的修饰,也就是说ViewGroup类只能用来被继承,无法实例化,并且其子类必须重载onLayout函数,而重载onLayout的目的就是安排其children在父视图的具体位置。重载onLayout通常做法就是起一个for循环调用每一个子视图的layout(l, t, r, b)函数,传入不同的参数l, t, r, b来确定每个子视图在父视图中的显示位置。
那layout(l, t, r, b)中的4个参数l, t, r, b如何来确定呢?联想到之前的measure过程,measure过程的最终结果就是确定了每个视图的mMeasuredWidth和mMeasuredHeight,这两个参数可以简单理解为视图期望在屏幕上显示的宽和高,而这两个参数为layout过程提供了一个很重要的依据(但不是必须的),为了说明这个过程,我们来看下LinearLayout的layout过程:

void layoutVertical() {
        ……
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
                ……
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }
private void setChildFrame(View child, int left, int top, int width, int height) {        
        child.layout(left, top, left + width, top + height);
    }

从setChildFrame可以看到LinearLayout中的子视图的右边界等于left + width,下边界等于top+height,也就是说在LinearLayout中其子视图显示的宽和高由measure过程来决定的,因此measure过程的意义就是为layout过程提供视图显示范围的参考值。

layout过程必须要依靠measure计算出来的mMeasuredWidth和mMeasuredHeight来决定视图的显示大小吗?事实并非如此,layout过程中的4个参数l, t, r, b完全可以由视图设计者任意指定,而最终视图的布局位置和大小完全由这4个参数决定,measure过程得到的mMeasuredWidth和mMeasuredHeight提供了视图大小的值,但我们完全可以不使用这两个值,可见measure过程并不是必须的。\
说到这里就不得不提getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight()这两对函数之间的区别,getMeasuredWidth()、getMeasuredHeight()返回的是measure过程得到的mMeasuredWidth和mMeasuredHeight的值,而getWidth()和getHeight()返回的是mRight - mLeft和mBottom - mTop的值,看View.java中的源码便一清二楚了:

public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }
public final int getWidth() {
        return mRight - mLeft;
    }

这也解释了为什么有些情况下getWidth()和getMeasuredWidth()以及getHeight()和getMeasuredHeight()会得到不同的值。

总结:整个layout过程比较容易理解,一般情况下layout过程会参考measure过程中计算得到的mMeasuredWidth和mMeasuredHeight来安排子视图在父视图中显示的位置,但这不是必须的,measure过程得到的结果可能完全没有实际用处,特别是对于一些自定义的ViewGroup,其子视图的个数、位置和大小都是固定的,这时候我们可以忽略整个measure过程,只在layout函数中传入的4个参数来安排每个子视图的具体位置。

<think>我们被要求详细介绍`androidx.appcompat.view`包。根据Android官方文档,这个包主要包含与视图(View)和窗口(Window)相关的兼容性支持类。它提供了一些向后兼容的类,用于处理ActionBar、ContextMenu和窗口装饰等。 主要类解析: 1. `ActionBar`:提供兼容性支持的ActionBar实现。 2. `ActionMode`:用于实现上下文操作栏(Contextual Action Bar)的兼容类。 3. `ContextThemeWrapper`:允许动态修改主题的Context包装类。 4. `WindowDecorActionBar`:ActionBar的一个具体实现,用于装饰窗口。 另外,这个包中还有一个重要的类:`StandaloneActionMode`,它是`ActionMode`的一个实现,用于在没有ActionBar的情况下独立使用。 注意:在AppCompat库中,通常我们使用`AppCompatActivity`,它会自动使用这些兼容类来提供一致的行为。 下面我们结合引用中提到的几个问题,看看这个包如何解决问题: 引用[1]提到:`View androidx.appcompat.widget.AppCompatImageView does not have a NavController set`。这通常是因为在Dialog中尝试获取NavController,而Dialog不属于任何NavHostFragment。解决方法是确保在正确的Fragment中获取NavController。虽然这个问题不直接与`androidx.appcompat.view`相关,但提醒我们在使用View时要注意上下文。 引用[2]是关于Toolbar的使用,它属于`androidx.appcompat.widget`包,但是我们在Activity中可以通过`androidx.appcompat.view`包中的`ActionBar`相关类来设置支持ActionBar。 引用[3]和[4]是关于控件使用时的常见错误,比如SwitchCompat需要设置textOff和textOn属性避免空指针,以及布局文件中的类找不到问题。 现在,我们重点介绍`androidx.appcompat.view`中的核心类和方法。 ### 1. ActionBar 在Android 3.0(API 11)及以上,系统提供了ActionBar,但为了在低版本中使用,AppCompat库提供了兼容的实现。 ```java // 在Activity中获取ActionBar ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); // 显示返回按钮 actionBar.setTitle("标题"); // 设置标题 } ``` ### 2. ActionMode ActionMode用于实现上下文操作(比如选择文本后的操作栏)。在AppCompat中,可以通过以下方式启动: ```java // 启动ActionMode ActionMode actionMode = startSupportActionMode(new ActionMode.Callback() { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { // 创建菜单 mode.getMenuInflater().inflate(R.menu.context_menu, menu); return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { // 处理菜单项点击 return false; } @Override public void onDestroyActionMode(ActionMode mode) { // 销毁时清理 } }); ``` ### 3. ContextThemeWrapper 这个类允许我们在运行时改变主题。例如,我们可以动态设置View的主题: ```java // 创建带有新主题的Context ContextThemeWrapper contextThemeWrapper = new ContextThemeWrapper(context, R.style.NewTheme); // 然后用这个Context来创建View View view = LayoutInflater.from(contextThemeWrapper).inflate(R.layout.layout, parent, false); ``` ### 4. 其他支持类 - `MenuInflater`:用于从XML文件加载菜单。 - `View`相关的支持类,但注意视图控件主要在`androidx.appcompat.widget`包中。 ### 常见问题与解决 1. **ActionBar不显示**: - 确保Activity主题是AppCompat主题(如`Theme.AppCompat.Light.DarkActionBar`)[^2]。 - 检查是否调用了`getSupportActionBar()`或是否正确设置了Toolbar作为ActionBar。 2. **ActionMode不显示**: - 确保在正确的View上触发了ActionMode,并且回调方法返回true。 3. **主题不生效**: - 使用`ContextThemeWrapper`时注意,它不会影响整个Activity,只影响用它创建的View。 ### 最佳实践 - 使用`AppCompatActivity`代替普通的`Activity`,以便使用兼容的ActionBar和ActionMode。 - 使用`AppCompatDelegate`来控制夜间模式等全局UI配置。 ```java // 启用夜间模式 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); ``` ### 与导航组件的交互 引用[1]中提到的错误是因为在Dialog中使用导航组件导致的。注意,导航组件的作用域是NavHostFragment,所以确保在Fragment或Activity中获取NavController。 ```java // 在Fragment中安全获取NavController NavController navController = NavHostFragment.findNavController(this); ``` ### 小结 `androidx.appcompat.view`包提供了视图和窗口相关的兼容支持,特别是ActionBar和ActionMode的兼容实现。它使得应用在旧版Android系统上也能具有现代的外观和功能。 --- §§相关问题§§ 1. 如何在低版本Android系统中实现ActionBar? 2. `ActionMode`和`Contextual Action Bar`有什么区别? 3. 如何动态改变Activity的主题? 4. 为什么使用`ContextThemeWrapper`而不直接修改Activity主题? </think>### `androidx.appcompat.view` 包详解 `androidx.appcompat.view` 是 AndroidX AppCompat 库的重要组成部分,主要提供兼容性视图支持和高级 UI 控制功能。它在不同 Android 版本间保持一致的 UI 行为,特别是在处理 ActionBar、ContextMenu 和视图主题定制方面。 --- ### 一、核心功能与组件 #### 1. **ActionMode 支持** 提供上下文操作栏(Contextual Action Bar)的兼容实现: ```kotlin val actionModeCallback = object : ActionMode.Callback { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { mode.menuInflater.inflate(R.menu.context_menu, menu) return true } // 其他回调方法... } startSupportActionMode(actionModeCallback) // 启动上下文操作模式 ``` - 在 API 11 以下设备自动模拟上下文操作栏 - 与 `AppCompatActivity` 无缝集成 #### 2. **上下文菜单兼容** 解决旧设备上上下文菜单的渲染问题: ```xml <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/delete" android:title="删除" app:showAsAction="ifRoom"/> <!-- app:前缀确保兼容性 --> </menu> ``` #### 3. **主题封装器** 动态修改视图主题: ```java Context themedContext = new ContextThemeWrapper( baseContext, R.style.NightModeTheme ); View inflatedView = LayoutInflater.from(themedContext) .inflate(R.layout.custom_view, parentView, false); ``` --- ### 二、引用问题解决方案 #### 1. **导航控制器错误修复** [^1] ```java // 正确获取 Fragment 中的 NavController NavHostFragment.findNavController(hostFragment) .navigate(R.id.targetFragment) // 错误做法(导致引用[1]的崩溃): view.findViewById(R.id.imageView).setOnClickListener { Navigation.findNavController(it).navigate(...) // 可能触发 View does not have NavController } ``` #### 2. **Toolbar 兼容处理** [^2] ```xml <!-- 正确声明 Toolbar --> <androidx.appcompat.widget.Toolbar android:id="@+id/tool_bar" app:title="兼容标题" <!-- 使用 app: 命名空间 --> app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/> ``` #### 3. **空指针异常预防** [^3] ```xml <androidx.appcompat.widget.SwitchCompat android:textOn="启用" <!-- 必须设置文本避免空指针 --> android:textOff="禁用"/> ``` #### 4. **布局渲染异常处理** [^4] ```xml <androidx.appcompat.widget.LinearLayoutCompat android:orientation="vertical" <!-- 明确声明必要属性 --> app:layout_constraintTop_toTopOf="parent"> <!-- 添加约束避免渲染崩溃 --> ``` --- ### 三、核心类解析 | 类名 | 功能 | 使用场景 | |------|------|----------| | `ActionMode` | 上下文操作栏控制 | 文本选择/批量操作 | | `ContextThemeWrapper` | 动态主题切换 | 夜间模式切换 | | `MenuInflater` | 菜单资源加载 | 创建上下文菜单 | | `StandaloneActionMode` | 独立操作栏实现 | 自定义上下文UI | --- ### 四、夜间模式实现 ```java // 在 Activity 中切换主题 AppCompatDelegate.setDefaultNightMode( AppCompatDelegate.MODE_NIGHT_YES ) // 响应主题变化 override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) applyTheme(recreate = true) // 重新应用主题 } ``` - 自动保留视图状态 - 兼容至 API 7 --- ### 五、最佳实践 1. **资源加载规范** ```kotlin // ✅ 使用兼容性菜单加载器 val inflater: MenuInflater = MenuInflater(ContextThemeWrapper(context, themeRes)) inflater.inflate(R.menu.actions, menu) // ❌ 直接使用 Activity 的 getMenuInflater() 可能导致旧设备异常 ``` 2. **ActionMode 生命周期管理** ```java class CustomView extends AppCompatTextView { private ActionMode mActionMode; public void startCustomActionMode() { mActionMode = startActionMode(new ActionMode.Callback(){...}); } @Override protected void onDetachedFromWindow() { if (mActionMode != null) { mActionMode.finish(); // 防止内存泄漏 } super.onDetachedFromWindow(); } } ``` --- ### 六、问题调试技巧 遇到 `View does not have NavController` 错误时: 1. 检查视图是否属于 Fragment 布局 2. 确认不在 Dialog 中使用 Fragment 导航 3. 使用 `NavHostFragment.findNavController()` 替代 `View.findNavController()` ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值