Подтвердить что ты не робот

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

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

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

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

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

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

4b9b3361

Ответ 1

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

Ответ 2

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

Ответ 3

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

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

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 onCreate***/
 ((CustomChildView)findViewById(R.id.customChildView)).SetOnResizeListener(orlResized);

Наконец, вы можете добавить свой CustomChildView в XML-макет, выполнив что-то вроде:

 <com.demo.CustomChildView>
      <!-- Attributes -->
 <com.demo.CustomChildView/>

Ответ 4

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

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

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

Некоторые другие ссылки для вас

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

Ответ 5

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