处理昂贵的视图高度计算的最佳实践?

发布于 2024-11-29 01:48:17 字数 1898 浏览 0 评论 0原文

我一直遇到自定义视图的大小和布局问题,我想知道是否有人可以建议“最佳实践”方法。问题如下。想象一个自定义视图,其中内容所需的高度取决于视图的宽度(类似于多行 TextView)。 (显然,这只适用于布局参数未固定高度的情况。)问题是,对于给定的宽度,计算这些自定义视图中的内容高度相当昂贵。特别是,在 UI 线程上计算成本太高,因此在某些时候需要启动工作线程来计算布局,并且当计算完成时,需要更新 UI。

问题是,这应该如何设计呢?我想了几个策略。他们都假设每当计算高度时,就会记录相应的宽度。

第一个策略如以下代码所示:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = measureWidth(widthMeasureSpec);
    setMeasuredDimension(width, measureHeight(heightMeasureSpec, width));
}

private int measureWidth(int widthMeasureSpec) {
    // irrelevant to this problem
}

private int measureHeight(int heightMeasureSpec, int width) {
    int result;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    if (specMode == MeasureSpec.EXACTLY) {
        result = specSize;
    } else {
        if (width != mLastWidth) {
            interruptAnyExistingLayoutThread();
            mLastWidth = width;
            mLayoutHeight = DEFAULT_HEIGHT;
            startNewLayoutThread();
        }
        result = mLayoutHeight;
        if (specMode == MeasureSpec.AT_MOST && result > specSize) {
            result = specSize;
        }
    }
    return result;
}

当布局线程完成时,它向 UI 线程发送一个 Runnable 以将 mLayoutHeight 设置为计算出的高度,然后调用 requestLayout() (和invalidate())。

第二种策略是让 onMeasure 始终使用 mLayoutHeight 的当前值(不启动布局线程)。测试宽度变化并启动布局线程可以通过重写onSizeChanged来完成。

第三种策略是偷懒并等待在 onDraw 中启动布局线程(如果需要)。

我想最大限度地减少布局线程启动和/或终止的次数,同时尽快计算所需的高度。尽量减少对 requestLayout() 的调用次数也可能会更好。

从文档中可以清楚地看出,在单个布局过程中可能会多次调用 onMeasure 。不太清楚(但似乎有可能),onSizeChanged 也可能被调用多次。所以我认为将逻辑放在 onDraw 中可能是更好的策略。但这似乎违背了自定义视图大小调整的精神,因此我承认对它有非理性的偏见。

其他人一定也遇到过同样的问题。有没有我错过的方法?有最好的方法吗?

I keep running into a sizing and layout problem for custom views and I'm wondering if anyone can suggest a "best practices" approach. The problem is as follows. Imagine a custom view where the height required for the content depends on the width of the view (similar to a multi-line TextView). (Obviously, this only applies if the height isn't fixed by the layout parameters.) The catch is that for a given width, it's rather expensive to compute the content height in these custom views. In particular, it's too expensive to be computed on the UI thread, so at some point a worker thread needs to be fired up to compute the layout and when it is finished, the UI needs to be updated.

The question is, how should this be designed? I've thought of several strategies. They all assume that whenever the height is calculated, the corresponding width is recorded.

The first strategy is shown in this code:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = measureWidth(widthMeasureSpec);
    setMeasuredDimension(width, measureHeight(heightMeasureSpec, width));
}

private int measureWidth(int widthMeasureSpec) {
    // irrelevant to this problem
}

private int measureHeight(int heightMeasureSpec, int width) {
    int result;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    if (specMode == MeasureSpec.EXACTLY) {
        result = specSize;
    } else {
        if (width != mLastWidth) {
            interruptAnyExistingLayoutThread();
            mLastWidth = width;
            mLayoutHeight = DEFAULT_HEIGHT;
            startNewLayoutThread();
        }
        result = mLayoutHeight;
        if (specMode == MeasureSpec.AT_MOST && result > specSize) {
            result = specSize;
        }
    }
    return result;
}

When the layout thread finishes, it posts a Runnable to the UI thread to set mLayoutHeight to the calculated height and then call requestLayout() (and invalidate()).

A second strategy is to have onMeasure always use the then-current value for mLayoutHeight (without firing up a layout thread). Testing for changes in width and firing up a layout thread would be done by overriding onSizeChanged.

A third strategy is to be lazy and wait to fire up the layout thread (if necessary) in onDraw.

I would like to minimize the number of times a layout thread is launched and/or killed, while also calculating the required height as soon as possible. It would probably be good to minimize the number of calls to requestLayout() as well.

From the docs, it's clear that onMeasure might be called several times during the course of a single layout. It's less clear (but seems likely) that onSizeChanged might also be called several times. So I'm thinking that putting the logic in onDraw might be the better strategy. But that seems contrary to the spirit of custom view sizing, so I have an admittedly irrational bias against it.

Other people must have faced this same problem. Are there approaches I've missed? Is there a best approach?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

尸血腥色 2024-12-06 01:48:17

我认为 Android 中的布局系统并不是真正为解决这样的问题而设计的,这可能会建议改变问题。

也就是说,我认为这里的核心问题是你的视图实际上并不负责计算它自己的高度。始终由视图的父视图计算其子视图的尺寸。他们可以表达自己的“意见”,但最终,就像在现实生活中一样,他们对此事没有任何真正的发言权。

这建议查看视图的父级,或者更确切地说,查看其尺寸独立于其子级尺寸的第一个父级。该父级可以拒绝布局(从而绘制)其子级,直到所有子级完成其测量阶段(这发生在单独的线程中)。一旦完成,父级就会请求一个新的布局阶段并布局其子级,而无需再次测量它们。

重要的是,子级的测量不会影响所述父级的测量,以便它可以“吸收”第二布局阶段,而不必重新测量其子级,从而解决布局过程。

[编辑]
对此进行一点扩展,我可以想到一个非常简单的解决方案,但实际上只有一个小缺点。您可以简单地创建一个扩展 ViewGroupAsyncView,并且与 ScrollView 类似,只包含一个始终填充其整个空间的子级。 AsyncView 不考虑其子级的尺寸,理想情况下只是填充可用空间。 AsyncView 所做的就是将其子级的测量调用包装在一个单独的线程中,该线程在测量完成后立即回调视图。

在该视图中,您几乎可以放置任何您想要的内容,包括其他布局。 “有问题的观点”在层次结构中有多深并不重要。唯一的缺点是,在测量所有后代之前,不会渲染任何后代。但您可能希望显示某种加载动画,直到视图准备好为止。

“有问题的视图”不需要以任何方式关心多线程。它可以像任何其他视图一样测量自身,需要多少时间就花多少时间。

[编辑2]
我什至费尽心思编写了一个快速实现:

package com.example.asyncview;

import android.content.Context;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

public class AsyncView extends ViewGroup {
    private AsyncTask<Void, Void, Void> mMeasureTask;
    private boolean mMeasured = false;

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

    public AsyncView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        for(int i=0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            child.layout(0, 0, child.getMeasuredWidth(), getMeasuredHeight());
        }
    }

    @Override
    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if(mMeasured)
            return;
        if(mMeasureTask == null) {
            mMeasureTask = new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(Void... objects) {
                    for(int i=0; i < getChildCount(); i++) {
                        measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
                    }
                    return null;
                }

                @Override
                protected void onPostExecute(Void aVoid) {
                    mMeasured = true;
                    mMeasureTask = null;
                    requestLayout();
                }
            };
            mMeasureTask.execute();
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        if(mMeasureTask != null) {
            mMeasureTask.cancel(true);
            mMeasureTask = null;
        }
        mMeasured = false;
        super.onSizeChanged(w, h, oldw, oldh);
    }
}

请参阅 https://github.com/wetblanket/AsyncView 获取工作示例

I think the layout system in Android wasn't really designed to solve a problem like this, which would probably suggest changing the problem.

That said, I think the central issue here is that your view isn't actually responsible for calculating its own height. It is always the parent of a view that calculates the dimensions of its children. They can voice their "opinion", but in the end, just like in real life, they don't really have any real say in the matter.

That would suggest taking a look at the parent of the view, or rather, the first parent whose dimensions are independent of the dimensions of its children. That parent could refuse layouting (and thus drawing) its children until all children have finished their measuring phase (which happens in a separate thread). As soon as they have, the parent requests a new layout phase and layouts its children without having to measure them again.

It is important that the measurements of the children don't affect the measurement of said parent, so that it can "absorb" the second layout phase without having to remeasure its children, and thus settle the layout process.

[edit]
Expanding on this a bit, I can think of a pretty simple solution that only really has one minor downside. You could simply create an AsyncView that extends ViewGroup and, similar to ScrollView, only ever contains a single child which always fills its entire space. The AsyncView does not regard the measurement of its child for its own size, and ideally just fills the available space. All that AsyncView does is wrap the measurement call of its child in a separate thread, that calls back to the view as soon as the measurement is done.

Inside of that view, you can pretty much put whatever you want, including other layouts. It doesn't really matter how deep the "problematic view" is in the hierarchy. The only downside would be that none of the descendants would get rendered until all of the descendants have been measured. But you probably would want to show some kind of loading animation until the view is ready anyways.

The "problematic view" would not need to concern itself with multithreading in any way. It can measure itself just like any other view, taking as much time as it needs.

[edit2]
I even bothered to hack together a quick implementation:

package com.example.asyncview;

import android.content.Context;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

public class AsyncView extends ViewGroup {
    private AsyncTask<Void, Void, Void> mMeasureTask;
    private boolean mMeasured = false;

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

    public AsyncView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        for(int i=0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            child.layout(0, 0, child.getMeasuredWidth(), getMeasuredHeight());
        }
    }

    @Override
    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if(mMeasured)
            return;
        if(mMeasureTask == null) {
            mMeasureTask = new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(Void... objects) {
                    for(int i=0; i < getChildCount(); i++) {
                        measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
                    }
                    return null;
                }

                @Override
                protected void onPostExecute(Void aVoid) {
                    mMeasured = true;
                    mMeasureTask = null;
                    requestLayout();
                }
            };
            mMeasureTask.execute();
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        if(mMeasureTask != null) {
            mMeasureTask.cancel(true);
            mMeasureTask = null;
        }
        mMeasured = false;
        super.onSizeChanged(w, h, oldw, oldh);
    }
}

See https://github.com/wetblanket/AsyncView for a working example

找个人就嫁了吧 2024-12-06 01:48:17

昂贵计算的一般方法是记忆化——缓存计算结果,希望那些结果可以再次使用。我不知道记忆在这里应用得有多好,因为我不知道相同的输入数字是否可以出现多次。

A general approach to expensive calculation is memoization -- caching the results of a calculation in the hope that those results can be used again. I don't know how well memoization applies here, because I don't know if the same input numbers can occur multiple times.

眼睛会笑 2024-12-06 01:48:17

您还可以执行类似以下策略的操作:

创建自定义子视图:

public class CustomChildView extends View
{
    MyOnResizeListener orl = null; 
    public CustomChildView(Context context)
    {
        super(context);
    }
    public CustomChildView(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }
    public CustomChildView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
    }

    public void SetOnResizeListener(MyOnResizeListener myOnResizeListener )
    {
        orl = myOnResizeListener;
    }

    @Override
    protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld)
    {
        super.onSizeChanged(xNew, yNew, xOld, yOld);

        if(orl != null)
        {
            orl.OnResize(this.getId(), xNew, yNew, xOld, yOld);
        }
    }
 }

并创建一些自定义侦听器,例如:

public class MyOnResizeListener
 {
    public MyOnResizeListener(){}

    public void OnResize(int id, int xNew, int yNew, int xOld, int yOld){}
 }

实例化侦听器,例如:

Class MyActivity extends Activity
{
      /***Stuff***/

     MyOnResizeListener orlResized = new MyOnResizeListener()
     {
          @Override
          public void OnResize(int id, int xNew, int yNew, int xOld, int yOld)
          {
/***Handle resize event and call your measureHeight(int heightMeasureSpec, int width) method here****/
          }
     };
}

并且不要忘记将侦听器传递到自定义视图:

 /***Probably in your activity's onCreate***/
 ((CustomChildView)findViewById(R.id.customChildView)).SetOnResizeListener(orlResized);

最后,您可以将 CustomChildView 添加到XML 布局通过执行以下操作:

 <com.demo.CustomChildView>
      <!-- Attributes -->
 <com.demo.CustomChildView/>

You Can also do Something Like this strategy:

Create Custom Child View:

public class CustomChildView extends View
{
    MyOnResizeListener orl = null; 
    public CustomChildView(Context context)
    {
        super(context);
    }
    public CustomChildView(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }
    public CustomChildView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
    }

    public void SetOnResizeListener(MyOnResizeListener myOnResizeListener )
    {
        orl = myOnResizeListener;
    }

    @Override
    protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld)
    {
        super.onSizeChanged(xNew, yNew, xOld, yOld);

        if(orl != null)
        {
            orl.OnResize(this.getId(), xNew, yNew, xOld, yOld);
        }
    }
 }

And create some Custom listener like :

public class MyOnResizeListener
 {
    public MyOnResizeListener(){}

    public void OnResize(int id, int xNew, int yNew, int xOld, int yOld){}
 }

You instantiate the listener like:

Class MyActivity extends Activity
{
      /***Stuff***/

     MyOnResizeListener orlResized = new MyOnResizeListener()
     {
          @Override
          public void OnResize(int id, int xNew, int yNew, int xOld, int yOld)
          {
/***Handle resize event and call your measureHeight(int heightMeasureSpec, int width) method here****/
          }
     };
}

And don't forget to pass your listener to your custom view:

 /***Probably in your activity's onCreate***/
 ((CustomChildView)findViewById(R.id.customChildView)).SetOnResizeListener(orlResized);

Finally, you can add your CustomChildView to an XML layout by doing something like:

 <com.demo.CustomChildView>
      <!-- Attributes -->
 <com.demo.CustomChildView/>
我家小可爱 2024-12-06 01:48:17

Android 在启动时并不知道实际大小,它需要计算它。完成后,onSizeChanged() 将通知您实际大小。

一旦计算出大小,就会调用onSizeChanged()。事件不必始终来自用户。当android改变尺寸时,onSizeChanged()被调用。与 onDraw() 相同,当应该绘制视图时 onDraw() 被调用。

onMeasure() 在调用 measure() 后立即自动调用

一些其他相关链接

顺便说一句,感谢您在这里提出如此有趣的问题。
很高兴能帮助你。:)

Android doesn't know the real size at start, it needs to calculate it. Once it's done, onSizeChanged() will notify you with the real size.

onSizeChanged() is called once the size as been calculated. Events don't have to come from users all the time. when android change the size, onSizeChanged() is called. And Same thing with onDraw(), when the view should be drawn onDraw() is called.

onMeasure() is called automatically right after a call to measure()

Some other related link for you

By the way,Thanks for asking such an interesting question here.
Happy to help.:)

对岸观火 2024-12-06 01:48:17

有操作系统提供的小部件、操作系统提供的事件/回调系统、操作系统提供的布局/约束管理、操作系统提供的与小部件的数据绑定。我将其统称为操作系统提供的 UI API。
无论操作系统提供的 UI API 的质量如何,您的应用程序有时可能无法使用其功能来有效解决。因为它们的设计可能考虑了不同的想法。
因此,总是有一个很好的选择来抽象并创建您自己的 - 在操作系统为您提供的 API 之上创建面向任务的 UI API。
您自己提供的 api 可以让您计算和存储,然后以更有效的方式使用不同的小部件尺寸。减少或消除瓶颈、线程间、回调等。
有时创建这样的layer-api只需几分钟,或者有时它可以足够先进,成为整个项目的最大部分。
长时间调试和支持它可能需要一些努力,这是这种方法的主要缺点。
但好处是你的心态会改变。你开始思考“热创造”而不是“如何找到绕过限制的方法”。
我不知道“最佳实践”,但这可能是最常用的方法之一。
您经常可以听到“我们使用我们自己的”。在自制和大佬提供之间做出选择并不容易。保持理智很重要。

There are OS-provided widgets, OS-provided event/callback system, OS-provided layout/constraint management, OS-provided data binding to widgets. Alltogether I call it OS-provided UI API.
Regardless of quality of OS-provided UI API, your app sometimes may not be effectively solved using it's features. Because they might have been designed with different ideas in mind.
So there is always a good option to abstract, and create your own - your task oriented UI API on top of the one that OS provides you.
Your own self provided api would let you calculate and store and then use different widget sizes in more efficient way. Reducing or removing bottlenecks, inter-threading, callbacks etc.
Sometimes creating such layer-api is matter of minutes, or sometimes it can be advanced enough to be the largest part of your whole project.
Debugging and supporting it over long periods of time may take some effort, this is main disadvantage of this approach.
But advantage is that your mind set changes. You start thinking "hot to create" instead of "how to find the way around limitations".
I do not know about "best practice" but this is probably one of the most used approach.
Often you can hear "we use our own". Choosing between self-made and big-guys provided is not easy. Keeping it sane is important.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文