Когда я могу впервые измерить вид?

Поэтому я немного запутался, пытаясь установить фон для рисования в том виде, в котором он отображается. Код полагается на знание высоты представления, поэтому я не могу вызвать его из onCreate() или же onResume(), так как getHeight() возвращает 0 onResume() кажется, самый близкий, который я могу получить все же. Где я должен поместить код, подобный приведенному ниже, чтобы фон изменялся при отображении для пользователя?

    TextView tv = (TextView)findViewById(R.id.image_test);
    LayerDrawable ld = (LayerDrawable)tv.getBackground();
    int height = tv.getHeight(); //when to call this so as not to get 0?
    int topInset = height / 2;
    ld.setLayerInset(1, 0, topInset, 0, 0);
    tv.setBackgroundDrawable(ld);

9 ответов

Решение

Я не знал о ViewTreeObserver.addOnPreDrawListener()и я попробовал это в тестовом проекте.

С вашим кодом это будет выглядеть так:

public void onCreate() {
setContentView(R.layout.main);

final TextView tv = (TextView)findViewById(R.id.image_test);
final LayerDrawable ld = (LayerDrawable)tv.getBackground();
final ViewTreeObserver obs = tv.getViewTreeObserver();
obs.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    @Override
    public boolean onPreDraw () {
        Log.d(TAG, "onPreDraw tv height is " + tv.getHeight()); // bad for performance, remove on production
        int height = tv.getHeight();
        int topInset = height / 2;
        ld.setLayerInset(1, 0, topInset, 0, 0);
        tv.setBackgroundDrawable(ld);

        return true;
   }
});
}

В моем тестовом проекте onPreDraw() был вызван дважды, и я думаю, что в вашем случае это может вызвать бесконечный цикл.

Вы можете попробовать позвонить setBackgroundDrawable() только когда высота TextView изменения:

private int mLastTvHeight = 0;

public void onCreate() {
setContentView(R.layout.main);

final TextView tv = (TextView)findViewById(R.id.image_test);
final LayerDrawable ld = (LayerDrawable)tv.getBackground();
final ViewTreeObserver obs = mTv.getViewTreeObserver();
obs.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    @Override
    public boolean onPreDraw () {
        Log.d(TAG, "onPreDraw tv height is " + tv.getHeight()); // bad for performance, remove on production
        int height = tv.getHeight();
        if (height != mLastTvHeight) {
            mLastTvHeight = height;
            int topInset = height / 2;
            ld.setLayerInset(1, 0, topInset, 0, 0);
            tv.setBackgroundDrawable(ld);
        }

        return true;
   }
});
}

Но это звучит немного сложно для того, чего вы пытаетесь достичь, и не очень хорошо для производительности.

РЕДАКТИРОВАТЬ kcoppock

Вот что я в итоге сделал из этого кода. Ответ Готье привел меня к этому моменту, поэтому я предпочел бы принять этот ответ с модификацией, чем отвечать на него сам. В итоге я вместо этого использовал метод addOnGlobalLayoutListener() в ViewTreeObserver, вот так (это в onCreate()):

final TextView tv = (TextView)findViewById(R.id.image_test);
ViewTreeObserver vto = tv.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        LayerDrawable ld = (LayerDrawable)tv.getBackground();
        ld.setLayerInset(1, 0, tv.getHeight() / 2, 0, 0);
    }
});

Кажется, работает отлично; Я проверил LogCat и не увидел никакой необычной активности. Надеюсь, это так! Спасибо!

По поводу наблюдателя дерева просмотров:

Возвращенный наблюдатель ViewTreeObserver не гарантированно остается действительным в течение всего времени существования этого представления. Если вызывающая сторона этого метода сохраняет долгоживущую ссылку на ViewTreeObserver, она всегда должна проверять возвращаемое значение isAlive().

Даже если это недолгая ссылка (используется только один раз, чтобы измерить представление и сделать то же самое, что вы делаете), я видел, что он больше не "жив" и выдает исключение. Фактически, каждая функция в ViewTreeObserver генерирует исключение IllegalStateException, если оно больше не "живое", поэтому вам всегда нужно проверять "isAlive". Я должен был сделать это:

    if(viewToMeasure.getViewTreeObserver().isAlive()) {
        viewToMeasure.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if(viewToMeasure.getViewTreeObserver().isAlive()) {
                    // only need to calculate once, so remove listener
                    viewToMeasure.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                }

                // you can get the view's height and width here

                // the listener might not have been 'alive', and so might not have been removed....so you are only
                // 99% sure the code you put here will only run once.  So, you might also want to put some kind
                // of "only run the first time called" guard in here too                                        

            }
        });            
    }

Это кажется довольно хрупким для меня. Многие вещи - хотя и редко - кажется, могут пойти не так.

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

 viewToMeasure.post(new Runnable() {

        @Override
        public void run() {
            // safe to get height and width here               
        }

    });

У меня не было проблем со стратегией публикации представления, и я не видел мерцаний на экране или других вещей, которые вы ожидали, могли бы пойти не так (пока...)

У меня были сбои в рабочем коде, потому что мой глобальный обозреватель макетов не был удален или потому что наблюдатель макета не был "живым", когда я пытался получить к нему доступ

Используя глобальный слушатель, вы можете столкнуться с бесконечным циклом.

я исправил это, позвонив

whateverlayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);

внутри метода onGlobalLayout внутри слушателя.

В своих проектах я использую следующий фрагмент:

public static void postOnPreDraw(Runnable runnable, View view) {
    view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            try {
                runnable.run();
                return true;
            } finally {
                view.getViewTreeObserver().removeOnPreDrawListener(this);
            }
        }
    });
}

Итак, внутри предусмотрено Runnable#run Вы можете быть уверены, что View был измерен и выполнен связанный код.

Вы можете переопределить onMeasure для TextView:

public MyTextView extends TextView {
   ...
   public void onMeasure (int widthMeasureSpec, int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec,heightMeasureSpec); // Important !!!
      final int width = getMeasuredHeight();
      final int height = getMeasuredHeight();
      // do you background stuff
   }
   ...
}

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

Итак, в общем, 3 способа обработки размера зрения.... хм, может быть, так!

1) Как отметил @Gautier Hayoun, с помощью слушателя OnGlobalLayoutListener. Однако, пожалуйста, не забудьте вызвать первый код в слушателе: listViewProduct.getViewTreeObserver().removeOnGlobalLayoutListener(this); чтобы убедиться, что этот слушатель не будет звонить бесконечно. И еще одна вещь, иногда этот слушатель не будет вызывать вообще, потому что ваш взгляд был нарисован там, где вы не знаете. Итак, чтобы быть в безопасности, давайте зарегистрируем этот слушатель сразу после того, как вы объявите представление в методе onCreate() (или onViewCreated() во фрагменте)

view = ([someView]) findViewById(R.id.[viewId]);

// Get width of view before it's drawn.
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
      @Override
      public void onGlobalLayout() {

        // Make this listener call once.
        view.getViewTreeObserver().removeOnGlobalLayoutListener(this);

        int width = listViewProduct.getWidth();
      });

2) Если вы хотите сделать что-то сразу после того, как представление было нарисовано и отображено, просто используйте метод post (конечно, вы можете получить размер здесь, так как ваше представление уже нарисовано полностью). Например,

listViewProduct.post(new Runnable() {
  @Override
  public void run() {
    // Do your stuff here.
  }
});

Более того, вы можете использовать postDelay с той же целью, добавив немного времени задержки. Давайте попробуем сами. Вам это понадобится когда-нибудь. Например, если вы хотите автоматически прокрутить представление прокрутки после добавления дополнительных элементов в представление прокрутки или развернуть или сделать еще один вид более видимым из пропавшего состояния в представлении прокрутки с анимацией (это означает, что этот процесс займет немного времени, чтобы показать все). Это определенно изменит размер вида прокрутки, поэтому после отрисовки вида прокрутки нам также нужно немного подождать, чтобы получить точный размер, после того как он добавит в него больше вида. Самое время, чтобы метод postDelay пришел на помощь!

3) И если вы хотите настроить свое собственное представление, вы можете создать класс и расширить его из любого понравившегося вам представления, у которого у всех есть метод onMeasure() для определения размера представления.

Надеюсь это поможет,

Mttdat.

      textView.doOnPreDraw {
   //call textView.height
}

Вы также можете использовать это расширение представления:

      androidx.core.view (ViewKt.class)

public inline fun View.doOnPreDraw(
    crossinline action: (View) → Unit
): OneShotPreDrawListener

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

Вы можете получить все показатели зрения в методе основной деятельности класса ниже метода onCreate():

@Override
protected void onCreate(Bundle savedInstanceState){}
@Override
public void onWindowFocusChanged(boolean hasFocus){
    int w = yourTextView.getWidth();
}

Так что вы можете перерисовывать, сортировать... ваши взгляды.:)

Использование OnPreDrawListener() вместо addOnGlobalLayoutListener()потому что это называется раньше.

  tv.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
    {
        @Override
        public boolean onPreDraw()
        {
            tv.getViewTreeObserver().removeOnPreDrawListener(this);
            // put your measurement code here
            return false;
        }
    });
Другие вопросы по тегам