Лучшие практики для работы с дорогостоящим расчетом высоты просмотра?

Я продолжаю сталкиваться с проблемой размеров и компоновки для пользовательских представлений, и мне интересно, может ли кто-нибудь предложить подход «лучших практик». Проблема заключается в следующем. Представьте себе пользовательское представление, в котором высота, необходимая для содержимого, зависит от ширины представления (аналогично многострочному TextView). (Очевидно, это применимо только в том случае, если высота не зафиксирована параметрами макета.) Загвоздка в том, что для заданной ширины довольно дорого вычислять высоту контента в этих пользовательских представлениях. В частности, вычисление в потоке пользовательского интерфейса слишком затратно, поэтому в какой-то момент необходимо запустить рабочий поток для вычисления макета, а когда он будет завершен, пользовательский интерфейс необходимо обновить.

Вопрос в том, как это должно быть оформлено? Я придумал несколько стратегий. Все они предполагают, что всякий раз, когда вычисляется высота, записывается соответствующая ширина.

Первая стратегия показана в этом коде:

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 в поток пользовательского интерфейса, чтобы установить mLayoutHeight в вычисленную высоту, а затем вызывает requestLayout()invalidate()).

Вторая стратегия заключается в том, чтобы onMeasure всегда использовало текущее значение для mLayoutHeight (без запуска потока компоновки). Тестирование изменений ширины и запуск потока компоновки можно выполнить, переопределив onSizeChanged.

Третья стратегия состоит в том, чтобы быть ленивым и ждать запуска потока компоновки (при необходимости) в onDraw.

Я хотел бы свести к минимуму количество запусков и/или уничтожений потока макета, а также как можно скорее вычислить требуемую высоту. Вероятно, было бы неплохо также минимизировать количество обращений к requestLayout().

Из документов ясно, что onMeasure может вызываться несколько раз в течение одного макета. Менее ясно (но кажется вероятным), что onSizeChanged также может вызываться несколько раз. Поэтому я думаю, что размещение логики в onDraw может быть лучшей стратегией. Но это кажется противоречащим духу пользовательских размеров представления, поэтому у меня, по общему признанию, иррациональное предубеждение против этого.

Другие люди, должно быть, столкнулись с той же проблемой. Есть ли подходы, которые я пропустил? Есть ли лучший подход?


person Ted Hopp    schedule 10.08.2011    source источник
comment
Разве нельзя было бы использовать OnGlobalLayoutListener() в представлении, высота которого требуется?   -  person lokoko    schedule 07.02.2013
comment
@lokoko - я не уверен, что понял предложение. Можете ли вы уточнить? Проблема в том, когда я должен инициировать дорогостоящие расчеты высоты макета?   -  person Ted Hopp    schedule 07.02.2013


Ответы (5)


Я думаю, что система компоновки в Android на самом деле не была предназначена для решения такой проблемы, что, вероятно, предполагает изменение проблемы.

Тем не менее, я думаю, что главная проблема здесь заключается в том, что ваше представление на самом деле не отвечает за вычисление собственной высоты. Это всегда родитель представления, который вычисляет размеры своих дочерних элементов. Они могут высказать свое «мнение», но, в конце концов, как и в реальной жизни, они не имеют права голоса в этом вопросе.

Это предполагает рассмотрение родителя представления или, скорее, первого родителя, размеры которого не зависят от размеров его дочерних элементов. Этот родитель может отказаться от компоновки (и, следовательно, отрисовки) своих дочерних элементов, пока все дочерние элементы не завершат свою фазу измерения (что происходит в отдельном потоке). Как только они это сделают, родитель запрашивает новую фазу макета и размещает свои дочерние элементы без необходимости их повторного измерения.

Важно, чтобы измерения дочерних элементов не влияли на измерения упомянутого родителя, чтобы он мог «поглотить» вторую фазу макета без необходимости повторного измерения своих дочерних элементов и, таким образом, установить процесс макета.

[edit] Немного расширив это, я могу придумать довольно простое решение, у которого действительно есть только один незначительный недостаток. Вы можете просто создать 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.

person Timo Ohr    schedule 07.02.2013
comment
Это очень полезно. Он ограничивает, где можно использовать пользовательский вид (только в макете, который знает, с чем имеет дело). Это также может быть довольно сложно, если осведомленный макет не является непосредственным родителем пользовательского макета; однако я думаю, что с этим можно справиться. Это также упрощает вопрос о том, когда запускать поток расчета измерений. Недостатком, однако, является то, что стандартные классы компоновки, как правило, не могут быть использованы. Здесь много пищи для размышлений. Спасибо. - person Ted Hopp; 07.02.2013
comment
Я немного расширил ответ - person Timo Ohr; 08.02.2013

Общий подход к дорогостоящим вычислениям – это запоминание — кэширование результатов вычислений в надежде, что эти результаты можно использовать повторно. Я не знаю, насколько хорошо здесь применима мемоизация, потому что я не знаю, могут ли одни и те же вводимые числа встречаться несколько раз.

person Mark Leighton Fisher    schedule 08.02.2013
comment
Да, мемоизация была бы жизненно важна в этом случае. Код, который я разместил, на самом деле использует мемоизацию (переменная mLayoutHeight), хотя я не называл эту технику по имени. - person Ted Hopp; 10.02.2013

Вы также можете использовать что-то вроде этой стратегии:

Создать собственное дочернее представление:

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/>
person shridutt kothari    schedule 07.02.2013
comment
Я не уверен, как это помогает. Я не беспокоюсь о том, чтобы реагировать на изменение размера в пользовательском макете. Проблема заключается в том, как установить измеренный размер пользовательского представления (что-то, что должно произойти как часть процесса компоновки — до того, как размер будет фактически установлен родителем представления). - person Ted Hopp; 07.02.2013

Android не знает реального размера при запуске, ему нужно его вычислить. Как только это будет сделано, onSizeChanged() сообщит вам реальный размер.

onSizeChanged() вызывается после расчета размера. События не обязательно должны исходить от пользователей все время. когда андроид меняет размер, вызывается onSizeChanged(). И то же самое с onDraw(), когда вид должен быть отрисован, вызывается onDraw().

onMeasure() вызывается автоматически сразу после вызова measure()

Другие ссылки по теме

Кстати, спасибо, что задали здесь такой интересный вопрос. Рад помочь.:)

person shridutt kothari    schedule 07.02.2013

Существуют виджеты, предоставляемые ОС, система событий/обратных вызовов, предоставляемая ОС, управление макетом/ограничениями, предоставляемое ОС, привязка данных к виджетам, предоставляемая ОС. В целом я называю это UI API, предоставляемым ОС. Независимо от качества API пользовательского интерфейса, предоставляемого ОС, иногда ваше приложение может быть неэффективно решено с использованием его функций. Потому что они могли быть разработаны с разными идеями. Таким образом, всегда есть хороший вариант абстрагироваться и создать свой собственный API-интерфейс пользовательского интерфейса, ориентированный на задачу, поверх того, который предоставляет вам ОС. Ваш собственный API позволит вам вычислять и хранить, а затем более эффективно использовать различные размеры виджетов. Сокращение или устранение узких мест, межпоточности, обратных вызовов и т. д. Иногда создание такого API-слоя занимает несколько минут, а иногда его можно сделать достаточно продвинутым, чтобы он стал самой большой частью всего вашего проекта. Отладка и поддержка в течение длительного периода времени могут потребовать некоторых усилий, это главный недостаток этого подхода. Но преимущество в том, что ваш образ мыслей меняется. Вы начинаете думать «горячо создавать» вместо «как обойти ограничения». Я не знаю о «лучшей практике», но это, вероятно, один из наиболее часто используемых подходов. Часто можно услышать «своим пользуемся». Выбрать между самодельными и обеспеченными большими парнями непросто. Важно сохранить его в здравом уме.

person exebook    schedule 09.02.2013