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

Ted*_*opp 16 layout android 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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

第三种策略是懒惰并等待启动布局线程(如果需要)onDraw.

我想尽量减少布局线程的启动和/或杀死次数,同时尽快计算所需的高度.最小化呼叫次数可能会很好requestLayout().

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

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

Tim*_*Ohr 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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