Лучшие практики для работы с дорогим расчетом высоты вида? - PullRequest
16 голосов
/ 10 августа 2011

Я постоянно сталкиваюсь с проблемой размеров и макета для пользовательских представлений, и мне интересно, может ли кто-нибудь предложить подход "передового опыта".Проблема заключается в следующем.Представьте себе пользовательское представление, в котором высота, требуемая для содержимого, зависит от ширины представления (аналогично многострочному 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 может быть лучшей стратегией.Но это кажется противоречащим духу определения размеров обычного представления, поэтому у меня, по общему признанию, иррациональное предубеждение против него.

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

Ответы [ 5 ]

10 голосов
/ 07 февраля 2013

Я думаю, что система макетов в 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 для рабочего примера

1 голос
/ 08 февраля 2013

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

0 голосов
/ 09 февраля 2013

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

0 голосов
/ 07 февраля 2013

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

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

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

Другая ссылка для вас

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

0 голосов
/ 07 февраля 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/>
...