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

时间:2011-08-10 17:24:52

标签: android layout custom-view

我一直在考虑自定义视图的大小调整和布局问题,我想知道是否有人可以提出“最佳实践”方法。问题如下。想象一下自定义视图,其中内容所需的高度取决于视图的宽度(类似于多行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;
}

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

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

第三种策略是懒惰并等待onDraw中的布局线程(如有必要)。

我想尽量减少布局线程的启动和/或杀死次数,同时尽快计算所需的高度。最好尽量减少对requestLayout()的调用次数。

从文档中可以清楚地看到,在单个布局过程中可能会多次调用onMeasure。不太清楚(但似乎很可能)onSizeChanged也可能被多次调用。所以我认为将逻辑放在onDraw可能是更好的策略。但这似乎与定制观点大小的精神背道而驰,所以我对此持有一种不合理的偏见。

其他人必须面对同样的问题。有没有我错过的方法?有最佳方​​法吗?

5 个答案:

答案 0 :(得分:10)

我认为Android中的布局系统并非真正设计用于解决这样的问题,这可能会改变问题。

那就是说,我认为这里的核心问题是你的观点实际上不负责计算自己的身高。它始终是视图的父级,用于计算其子级的维度。他们可以表达他们的“意见”,但最终,就像在现实生活中一样,他们在这件事上并没有真正的发言权。

这将建议查看视图的父级,或者更确切地说,第一个父级的维度与其子级的维度无关。父母可以拒绝布置(并因此绘制)其子女,直到所有孩子完成他们的测量阶段(这发生在一个单独的线程中)。一旦有了,父母就会请求一个新的布局阶段并布置其子项,而不必再次测量它们。

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

<强> [编辑] 稍微扩展一点,我可以想到一个非常简单的解决方案,只有一个小的缺点。您只需创建一个扩展AsyncView的{​​{1}},与ViewGroup类似,只会包含一个始终填充整个空间的子项。 ScrollView并不认为其子项的大小有其自身的大小,理想情况下只是填补了可用空间。所有AsyncView所做的就是将其子项的测量调用包装在一个单独的线程中,一旦测量完成,就会回调该视图。

在该视图的内部,你几乎可以放任何你想要的东西,包括其他布局。层次结构中“有问题的视图”的深度并不重要。唯一的缺点是在所有后代都被测量之前,所有后代都不会被渲染。但是你可能想要显示某种加载动画,直到视图准备就绪。

“有问题的观点”不需要以任何方式关注多线程。它可以像任何其他视图一样测量自己,花费尽可能多的时间。

<强> [EDIT2] 我甚至不愿意快速实施快速实施:

AsyncView

有关工作示例,请参阅https://github.com/wetblanket/AsyncView

答案 1 :(得分:1)

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

答案 2 :(得分:0)

你也可以做这样的策略:

创建自定义子视图:

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/>

答案 3 :(得分:0)

Android开始时不知道实际大小,需要计算它。完成后,onSizeChanged()会以实际尺寸通知您。

一旦计算出大小,就会调用

onSizeChanged()。事件不一定来自用户。当android改变大小时,调用onSizeChanged()。与onDraw()相同的是,当应该绘制视图时onDraw()被调用。

在调用onMeasure()

后立即自动调用

measure()

Some other related link for you

顺便说一下,谢谢你在这里提出这样一个有趣的问题。 很乐意提供帮助。:)

答案 4 :(得分:0)

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

相关问题