在运行时确定 Android 视图的尺寸

141

我正在尝试在Android应用程序中创建视图后对其应用动画。为此,我需要确定当前视图的大小,然后设置一个动画来从当前大小缩放到新的大小。由于视图根据用户输入缩放到不同的大小,因此必须在运行时完成此操作。我的布局是在XML中定义的。

这似乎是一个简单的任务,并且有许多SO问题涉及此问题,但没有解决我的问题,显然。所以我可能漏掉了一些很明显的东西。我通过以下方式获取我的视图句柄:

ImageView myView = (ImageView) getWindow().findViewById(R.id.MyViewID);
这个方法可以正常工作,但是调用getWidth()getHeight()getMeasuredWidth()getLayoutParams().width等方法时,它们都返回0。我也尝试了在视图上手动调用measure(),然后调用getMeasuredWidth(),但没有效果。
我已经尝试在活动的onCreate()onPostCreate()中调用这些方法,并在调试器中检查对象。如何在运行时确定此视图的确切尺寸?

1
哦,还有一点我应该注意的是,视图本身绝对不会具有0的宽度/高度。它在屏幕上显示得很好。 - Nik Reiman
你能发布XML布局中的<ImageView ... />标签吗? - fhucho
1
我曾经在Stack Overflow上尝试了很多解决方案,直到我意识到在我的情况下,视图的尺寸与物理屏幕匹配(我的应用程序是“沉浸式”的,视图及其所有父级的宽度和高度都设置为match_parent)。在这种情况下,一个更简单的解决方案甚至可以在View被绘制之前安全地调用(例如,在Activity的onCreate()方法中),只需使用Display.getSize()即可;有关详细信息,请参见https://dev59.com/tYzda4cB1Zd3GeqPiiM1#30929599。我知道这不是@NikReiman所要求的,但我留下一条注释给那些可能能够使用这种更简单方法的人。 - CODE-REaD
14个回答

196

使用ViewTreeObserver在View上等待第一次布局。只有在第一次布局之后,getWidth()/getHeight()/getMeasuredWidth()/getMeasuredHeight()才能正常工作。

ViewTreeObserver viewTreeObserver = view.getViewTreeObserver();
if (viewTreeObserver.isAlive()) {
  viewTreeObserver.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
      view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
      viewWidth = view.getWidth();
      viewHeight = view.getHeight();
    }
  });
}

24
请注意,如果底层视图是视频/表面,则应小心处理此答案。 - Kevin Parker
8
@Kevin,你能详细说明一下吗?首先,因为它是一个监听器,我希望只有一个回调函数。其次,一旦我们收到第一个回调,就要将该监听器移除。我有遗漏什么吗? - Vikram Bodicherla
4
在调用任何ViewTreeObserver方法之前,建议检查isAlive():if(view.getViewTreeObserver()。isAlive()){ view.getViewTreeObserver()。removeGlobalOnLayoutListener(this); } - Peter Tran
4
有没有其他替代方法来替换 removeGlobalOnLayoutListener(this);?因为它已经被弃用了。 - Sibbs Gambling
7
@FarticlePilter,使用removeOnGlobalLayoutListener()代替。文档中有提到。 - Vikram Bodicherla
显示剩余9条评论

81

实际上,根据场景,有多种解决方案:

  1. 安全的方法是在布局阶段结束后,在绘制视图之前使用:
public static void runJustBeforeBeingDrawn(final View view, final Runnable runnable) {
    final OnPreDrawListener preDrawListener = new OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            view.getViewTreeObserver().removeOnPreDrawListener(this);
            runnable.run();
            return true;
        }
    };
    view.getViewTreeObserver().addOnPreDrawListener(preDrawListener); 
}

使用示例:

    ViewUtil.runJustBeforeBeingDrawn(yourView, new Runnable() {
        @Override
        public void run() {
            //Here you can safely get the view size (use "getWidth" and "getHeight"), and do whatever you wish with it
        }
    });

在某些情况下,手动测量视图的大小就足够了:
view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
int width=view.getMeasuredWidth(); 
int height=view.getMeasuredHeight();

如果您知道容器的大小:

    val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(maxWidth, View.MeasureSpec.AT_MOST)
    val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(maxHeight, View.MeasureSpec.AT_MOST)
    view.measure(widthMeasureSpec, heightMeasureSpec)
    val width=view.measuredWidth
    val height=view.measuredHeight
  1. 如果你有一个扩展过的自定义视图,你可以在 "onMeasure" 方法中获取其大小,但我认为它只在某些情况下有效:
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
    final int newHeight= MeasureSpec.getSize(heightMeasureSpec);
    final int newWidth= MeasureSpec.getSize(widthMeasureSpec);
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
  1. If you write in Kotlin, you can use the next function, which behind the scenes works exactly like runJustBeforeBeingDrawn that I've written:

     view.doOnPreDraw { actionToBeTriggered() }
    

请注意,您需要将以下内容添加到gradle中(通过此处找到):

android {
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

implementation 'androidx.core:core-ktx:#.#'

1
似乎解决方案#1无法始终与OnPreDrawListener一起工作。测试用例是当您想要从Activity的onCreate()运行代码,并立即将应用程序放在后台时。这很容易重现,特别是在较慢的设备上。如果您将OnPreDrawListener替换为OnGlobalLayoutListener-则不会看到相同的问题。 - questioner
@questioner 我不明白,但我很高兴最终能帮到你。 - android developer
虽然我通常使用ViewTreeObserver或者post,但在我的情况下第二种方法很有帮助(view.measure)。 - CoolMind
3
如果您使用 Kotlin,您还可以尝试一种新的方法。更新的答案中也包括了它。 - android developer
为什么Kotlin需要这些额外的函数?Google太疯狂了,这种语言没用(应该还是用Java好),它比Swift早得多,但只有Swift足够受欢迎。https://www.tiobe.com/tiobe-index/(Swift排名第12,Kotlin排名第38) - user924
显示剩余17条评论

24

您是否在视图实际布局到屏幕上之前调用了getWidth()

新的Android开发人员常犯的一个错误是在视图构造函数中使用视图的宽度和高度。当调用视图的构造函数时,Android还不知道视图的大小,因此大小被设置为零。真正的大小是在布局阶段计算的,在构建之后但在任何内容绘制之前。您可以使用onSizeChanged()方法在值已知时收到通知,或稍后使用getWidth()getHeight()方法,例如在onDraw()方法中。


2
那么这是否意味着我需要重写 ImageView 才能捕获 onSizeChange() 事件以了解实际大小?这似乎有点过度设计...不过现在我已经绝望地想让代码工作起来,所以值得一试。 - Nik Reiman
我更倾向于等待您的Activity中的onCreate()方法执行完毕。 - Mark B
1
如前所述,我尝试将此代码放入onPostCreate()中,该方法在视图完全创建和启动后运行。 - Nik Reiman
1
引用的文本来自哪里? - Intrications
非常聪明的回答(不..), 覆盖所有使用的视图(创建自定义视图)并使用 onSizeChanged(),你是认真的吗? - user924
1
这是针对自定义视图的引用,当我们一般询问视图大小时,请不要将其作为答案发布。 - user924

16

根据@mbaird的建议,我通过子类化ImageView类并重写onLayout()方法找到了可行的解决方案。然后,我创建了一个观察者接口,我的Activity实现了该接口并将自身的引用传递给了该类,这使得它可以在完成调整大小时告知Activity。

我并不100%确信这是最好的解决方案(因此尚未将其标记为正确答案),但它确实有效,而且根据文档,这是第一次可以找到视图的实际大小。


好的,知道了。如果我找到任何可以避免子类化View的方法,我会在这里发布。我有点惊讶onPostCreate()没有起作用。 - Mark B
嗯,我尝试了一下,似乎可以工作。只是这不是最好的解决方案,因为这种方法经常被调用,而不仅仅是在开始时调用一次。 :( - Tobias Reich

10

这是一个获取布局的代码示例,用于覆盖视图(view)(适用于 API < 11 的版本。API 11及以上版本包括了View.OnLayoutChangedListener功能):

public class CustomListView extends ListView
{
    private OnLayoutChangedListener layoutChangedListener;

    public CustomListView(Context context)
    {
        super(context);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b)
    {
        if (layoutChangedListener != null)
        {
            layoutChangedListener.onLayout(changed, l, t, r, b);
        }
        super.onLayout(changed, l, t, r, b);
    }

    public void setLayoutChangedListener(
        OnLayoutChangedListener layoutChangedListener)
    {
        this.layoutChangedListener = layoutChangedListener;
    }
}
public interface OnLayoutChangedListener
{
    void onLayout(boolean changed, int l, int t, int r, int b);
}

7

5

这对我在onClickListener中起作用:

yourView.postDelayed(new Runnable() {               
    @Override
    public void run() {         
        yourView.invalidate();
        System.out.println("Height yourView: " + yourView.getHeight());
        System.out.println("Width yourView: " + yourView.getWidth());               
    }
}, 1);

4
当监听器为点击事件时,您不需要使用postDelayed方法。直到视图全部测量完成并显示在屏幕上之前,您无法单击任何内容。因此,在您能够单击它们之前,这些视图已经被测量过了。 - afollestad
这段代码是错误的。postDelay不能保证在下一个时钟周期中测量视图。 - Ian Wong
我建议不要使用postDelayed()。虽然这种情况不太可能发生,但如果您的视图在1秒内没有被测量,并且您调用此runnable来获取视图的宽度/高度,那么它仍将返回0。另外,顺便提一下,这里的1是毫秒,所以肯定会失败。 - Alex

5
使用下面的代码,它可以给出视图的尺寸大小。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
       super.onWindowFocusChanged(hasFocus);
       Log.e("WIDTH",""+view.getWidth());
       Log.e("HEIGHT",""+view.getHeight());
}

4
我曾经也困惑于getMeasuredWidth()getMeasuredHeight()getHeight()getWidth()等内容很长一段时间......后来我发现在onSizeChanged()中获取视图的宽度和高度是最好的方法。您可以通过覆盖onSizeChanged()方法来动态获取视图的当前宽度和高度。可能需要查看以下链接,其中有详细的代码片段:Android中如何获取自定义视图(扩展View)的宽度和高度尺寸的新博客文章:http://syedrakibalhasan.blogspot.com/2011/02/how-to-get-width-and-height-dimensions.html

1

您可以获取视图在屏幕上的位置和尺寸

 val viewTreeObserver: ViewTreeObserver = videoView.viewTreeObserver;

if (viewTreeObserver.isAlive) {
    viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            //Remove Listener
            videoView.viewTreeObserver.removeOnGlobalLayoutListener(this);

            //View Dimentions
            viewWidth = videoView.width;
            viewHeight = videoView.height;

            //View Location
            val point = IntArray(2)
            videoView.post {
                videoView.getLocationOnScreen(point) // or getLocationInWindow(point)
                viewPositionX = point[0]
                viewPositionY = point[1]
            }

        }
    });
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接