16

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

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

第三种策略是懒惰并等待在onDraw.

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

从文档中可以清楚地看出,onMeasure在单个布局过程中可能会被多次调用。onSizeChanged可能会被多次调用还不太清楚(但似乎很可能) 。所以我认为把逻辑放进去onDraw可能是更好的策略。但这似乎与自定义视图大小的精神背道而驰,所以我对它有一种公认的非理性偏见。

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

4

5 回答 5

10

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

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

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

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

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

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

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

[edit2] 我什至费心拼凑一个快速的实现:

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

于 2013-02-07T17:14:43.510 回答
1

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

于 2013-02-08T19:44:23.863 回答
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/>
于 2013-02-07T17:58:31.217 回答
0

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

onSizeChanged()一旦计算出大小,就会调用它。事件不必总是来自用户。当android改变大小时,onSizeChanged()被调用。和相同的事情onDraw(),当视图应该被onDraw()调用时。

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

为您提供的其他一些相关链接

顺便说一句,感谢您在这里提出如此有趣的问题。乐意效劳。:)

于 2013-02-07T18:08:39.197 回答
0

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

于 2013-02-09T05:41:47.320 回答