GetWidth() и getHeight() вида возвращают 0

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

Я провел некоторое исследование и понял, что это нужно сделать где-то, кроме как в onCreate() метод. Если кто-то может дать мне пример того, как это сделать, это было бы здорово.

Вот мой текущий код:

package com.animation;

import android.app.Activity;
import android.os.Bundle;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.Button;
import android.widget.LinearLayout;

public class AnimateScreen extends Activity {


//Called when the activity is first created.
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    LinearLayout ll = new LinearLayout(this);

    LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
    layoutParams.setMargins(30, 20, 30, 0);

    Button bt = new Button(this);
    bt.setText(String.valueOf(bt.getWidth()));

    RotateAnimation ra = new RotateAnimation(0,360,bt.getWidth() / 2,bt.getHeight() / 2);
    ra.setDuration(3000L);
    ra.setRepeatMode(Animation.RESTART);
    ra.setRepeatCount(Animation.INFINITE);
    ra.setInterpolator(new LinearInterpolator());

    bt.startAnimation(ra);

    ll.addView(bt,layoutParams);

    setContentView(ll);
}

Любая помощь приветствуется.

29 ответов

Решение

Ты звонишь getWidth() слишком рано. Пользовательский интерфейс еще не был измерен и выложен на экране.

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

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

Основная проблема заключается в том, что вам нужно ждать фазы рисования для фактических измерений (особенно с такими динамическими значениями, как wrap_content или же match_parent), но обычно этот этап не был завершен до onResume(), Так что вам нужен обходной путь для ожидания этого этапа. Существуют различные возможные решения этого:


1. Прослушивание событий Draw/Layout: ViewTreeObserver

ViewTreeObserver запускается для различных событий рисования. Обычно OnGlobalLayoutListener это то, что вы хотите получить для измерения, поэтому код в слушателе будет вызываться после фазы макета, поэтому измерения готовы:

view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                view.getHeight(); //height is ready
            }
        });

Примечание. Слушатель будет немедленно удален, так как в противном случае он будет срабатывать при каждом событии макета. Если вам нужно поддерживать приложения SDK Lvl < 16, используйте это, чтобы отменить регистрацию слушателя:

public void removeGlobalOnLayoutListener (ViewTreeObserver.OnGlobalLayoutListener victim)


2. Добавьте исполняемый файл в очередь макетов: View.post()

Не очень хорошо известно и мое любимое решение. По сути, просто используйте метод post представления View с вашим собственным runnable. Это в основном ставит ваш код в очередь после измерения представления, макета и т. Д., Как утверждает Romain Guy:

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

Пример:

final View view=//smth;
...
view.post(new Runnable() {
            @Override
            public void run() {
                view.getHeight(); //height is ready
            }
        });

Преимущество над ViewTreeObserver:

  • Ваш код выполняется только один раз, и вам не нужно отключать Observer после выполнения, что может быть хлопотно
  • менее подробный синтаксис

Рекомендации:


3. Перезаписать метод onLayout в представлениях

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

view = new View(this) {
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        view.getHeight(); //height is ready
    }
};

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


4. Проверьте, прошел ли этап макета

Если у вас есть код, который выполняется несколько раз при создании пользовательского интерфейса, вы можете использовать следующий метод поддержки v4 lib:

View viewYouNeedHeightFrom = ...
...
if(ViewCompat.isLaidOut(viewYouNeedHeightFrom)) {
   viewYouNeedHeightFrom.getHeight();
}

Возвращает true, если представление было выполнено хотя бы через один макет с момента последнего присоединения к окну или отсоединения от него.


Дополнительно: получение статически определенных измерений

Если достаточно просто получить статически определенную высоту / ширину, вы можете сделать это с помощью:

  • View.getMeasuredWidth()
  • View.getMeasuredHeigth()

Но учтите, что это может отличаться от фактической ширины / высоты после рисования. Javadoc отлично описывает разницу:

Размер представления выражается шириной и высотой. Представление фактически имеет две пары значений ширины и высоты.

Первая пара известна как измеренная ширина и измеренная высота. Эти измерения определяют, насколько большим должно быть представление в пределах его родителя (см. Layout для получения более подробной информации.) Измеренные измерения можно получить, вызвав getMeasuredWidth() и getMeasuredHeight().

Вторая пара просто называется шириной и высотой, а иногда шириной и высотой рисования. Эти размеры определяют фактический размер вида на экране, во время рисования и после макета. Эти значения могут, но не обязательно, отличаться от измеренной ширины и высоты. Ширина и высота могут быть получены путем вызова getWidth() и getHeight().

Мы можем использовать

@Override
 public void onWindowFocusChanged(boolean hasFocus) {
  super.onWindowFocusChanged(hasFocus);
  //Here you can get the size!
 }

Я использовал это решение, которое я считаю лучше, чем onWindowFocusChanged(). Если вы откроете DialogFragment, а затем поверните телефон, onWindowFocusChanged будет вызываться только тогда, когда пользователь закрывает диалоговое окно):

    yourView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

        @Override
        public void onGlobalLayout() {
            // Ensure you call it only once :
            yourView.getViewTreeObserver().removeGlobalOnLayoutListener(this);

            // Here you can get the size :)
        }
    });

Изменить: так как removeGlobalOnLayoutListener устарела, теперь вы должны сделать:

@SuppressLint("NewApi")
@SuppressWarnings("deprecation")
@Override
public void onGlobalLayout() {

    // Ensure you call it only once :
    if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
        yourView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
    }
    else {
        yourView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
    }

    // Here you can get the size :)
}

Если вам нужно получить ширину какого-либо виджета до его отображения на экране, вы можете использовать getMeasuredWidth() или getMeasuredHeight().

myImage.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
int width = myImage.getMeasuredWidth();
int height = myImage.getMeasuredHeight();

Как утверждает Ян в этой теме для разработчиков Android:

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

В итоге, если вы вызовете getWidth() и т. Д. В конструкторе, он вернет ноль. Процедура состоит в том, чтобы создать все элементы View в конструкторе, а затем дождаться вызова метода onSizeChanged() вашего View - именно тогда вы впервые узнаете свой реальный размер, и именно тогда вы зададите размеры элементов GUI.

Помните также, что onSizeChanged() иногда вызывается с параметрами, равными нулю - проверьте этот случай и немедленно вернитесь (чтобы вы не делили на ноль при расчете макета и т. Д.). Через некоторое время он будет вызван с реальными значениями.

Я бы предпочел использовать OnPreDrawListener() вместо addOnGlobalLayoutListener(), так как он называется раньше.

    view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
    {
        @Override
        public boolean onPreDraw()
        {
            if (view.getViewTreeObserver().isAlive())
                view.getViewTreeObserver().removeOnPreDrawListener(this);

            // put your code here
            return false;
        }
    });

AndroidX имеет несколько функций расширения, которые помогут вам в такой работе, внутри androidx.core.view

Для этого вам нужно использовать Kotlin.

Здесь лучше всего подходит doOnLayout:

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

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

В вашем примере:

bt.doOnLayout {
    val ra = RotateAnimation(0,360,it.width / 2,it.height / 2)
    // more code
}

Зависимость: androidx.core:core-ktx:1.0.0

Расширение для получения высоты в Котлине динамически.

Использование:

view.height { Log.i("Info", "Here is your height:" + it) }

Реализация:

fun <T : View> T.height(function: (Int) -> Unit) {
    if (height == 0)
        viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                viewTreeObserver.removeOnGlobalLayoutListener(this)
                function(height)
            }
        })
    else function(height)
}

Это происходит потому, что представление нужно больше времени, чтобы надуть. Так что вместо звонкаview.width а также view.height в основном потоке вы должны использовать view.post { ... } чтобы убедиться, что ваш viewуже был надут. В Котлине:

view.post{width}
view.post{height}

В Java вы также можете вызвать getWidth() а также getHeight() методы в Runnable и пройти Runnable к view.post() метод.

view.post(new Runnable() {
        @Override
        public void run() {
            view.getWidth(); 
            view.getHeight();
        }
    });

Один лайнер, если вы используете RxJava & RxBindings. Подобный подход без шаблонного. Это также решает проблему с подавлением предупреждений, как в ответе Тима Аутина.

RxView.layoutChanges(yourView).take(1)
      .subscribe(aVoid -> {
           // width and height have been calculated here
      });

Это оно. Не нужно отписываться, даже если никогда не звонил.

Может быть, это кому-то поможет:

Создайте функцию расширения для класса View

имя файла: ViewExt.kt

fun View.afterLayout(what: () -> Unit) {
    if(isLaidOut) {
        what.invoke()
    } else {
        viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                viewTreeObserver.removeOnGlobalLayoutListener(this)
                what.invoke()
            }
        })
    }
}

Затем это можно использовать в любом представлении с помощью:

view.afterLayout {
    do something with view.height
}

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

view.post(new Runnable() {
        @Override
        public void run() {
            view.getHeight(); //height is ready
            view.getWidth(); //width is ready
        }
    });

Этот метод хорош по сравнению с другими методами, поскольку он короткий и четкий.

Если вы используете Kotlin

  leftPanel.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {

            override fun onGlobalLayout() {

                if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
                    leftPanel.viewTreeObserver.removeOnGlobalLayoutListener(this)
                }
                else {
                    leftPanel.viewTreeObserver.removeGlobalOnLayoutListener(this)
                }

                // Here you can get the size :)
                leftThreshold = leftPanel.width
            }
        })

Ответить postневерно, так как размер не может быть пересчитан.
Еще одна важная вещь - это представление и все его предки должны быть видны. Для этого я использую свойствоView.isShown.

Вот моя функция kotlin, которую можно разместить где-нибудь в утилитах:

fun View.onInitialized(onInit: () -> Unit) {
    viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            if (isShown) {
                viewTreeObserver.removeOnGlobalLayoutListener(this)
                onInit()
            }
        }
    })
}

И использование:

myView.onInitialized {
    Log.d(TAG, "width is: " + myView.width)
}

Для Котлина:

Я столкнулся с производственной катастрофой из-за использования view.height/ view.widthкоторые ведут к NaNпока я использовал View.post()которые иногда просматривают размеры, возвращаемые со значением 0.

Так,

Использовать view.doOnPreDraw { // your action here}который:

  • OneShotPreDrawListenerтак он звонил только один раз.
  • Реализует OnPreDrawListenerкоторые гарантируют, что вид спланирован и измерен

Самый чистый способ сделать это - использовать почтовый метод просмотра:

котлин:

        view.post{
      var width = view.width
      var height = view.height
  }

Джава:

      view.post(new Runnable() {
    @Override
    public void run() {
        int width = view.getWidth();
        int height = view.getHeight();
    }
});

хорошо, вы можете использовать addOnLayoutChangeListener

вы можете использовать это в onCreate или onCreateView

      
myView.addOnLayoutChangeListener { view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> 
            Log.i(TAG, "view : ${view.width}")
        }

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

Предостережение здесь заключается в том, что вы не должны блокировать поток пользовательского интерфейса во время ожидания. Этого очень легко добиться, если вы используете сопрограммы Kotlin:

      override fun onStart() {
        super.onStart()
       
        coroutineScope.launch {
            while (imgBarcode.width == 0 || imgBarcode.height == 0) {
                MyLogger.d("waiting for ImageView to get its eventual size")
                delay(50)
            }

            val barcodeBitmap = generateBarcodeUseCase.generateBarcode(
                data, imgBarcode.width, imgBarcode.height
            )

            imgBarcode.setImageBitmap(barcodeBitmap)
        }
    }

Если вы не используете Coroutines, вам нужно будет использовать, например,Handler.postDelayed()вызовы для реализации той же функции.

Надо дождаться, когда вид будет нарисован. Для этого используйте OnPreDrawListener. Пример Kotlin:

val preDrawListener = object : ViewTreeObserver.OnPreDrawListener {

                override fun onPreDraw(): Boolean {
                    view.viewTreeObserver.removeOnPreDrawListener(this)

                    // code which requires view size parameters

                    return true
                }
            }

            view.viewTreeObserver.addOnPreDrawListener(preDrawListener)

Прошедшие просмотры возвращают 0 как высоту, если приложение в фоновом режиме. Это мой код (1оо% работает)

fun View.postWithTreeObserver(postJob: (View, Int, Int) -> Unit) {
    viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            val widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
            val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
            measure(widthSpec, heightSpec)
            postJob(this@postWithTreeObserver, measuredWidth, measuredHeight)
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                @Suppress("DEPRECATION")
                viewTreeObserver.removeGlobalOnLayoutListener(this)
            } else {
                viewTreeObserver.removeOnGlobalLayoutListener(this)
            }
        }
    })
}

Использование:

      imageView.size { width, height ->
            //your code
        }

Просмотреть расширение:

      
fun <T : View> T.size(function: (Int, Int) -> Unit) {
    if (isLaidOut && height != 0 && width != 0) {
        function(width, height)
    } else {
        if (height == 0 || width == 0) {
            var onLayoutChangeListener: View.OnLayoutChangeListener? = null
            var onGlobalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null

            onGlobalLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener {
                override fun onGlobalLayout() {
                    if (isShown) {
                        removeOnLayoutChangeListener(onLayoutChangeListener)
                        viewTreeObserver.removeOnGlobalLayoutListener(this)
                        function(width, height)
                    }
                }
            }

            onLayoutChangeListener = object : View.OnLayoutChangeListener {
                override fun onLayoutChange(
                    v: View?,
                    left: Int,
                    top: Int,
                    right: Int,
                    bottom: Int,
                    oldLeft: Int,
                    oldTop: Int,
                    oldRight: Int,
                    oldBottom: Int
                ) {
                    val width = v?.width ?: 0
                    val height = v?.height ?: 0
                    if (width > 0 && height > 0) {
                        // remove after finish
                        viewTreeObserver.removeOnGlobalLayoutListener(onGlobalLayoutListener)
                        v?.removeOnLayoutChangeListener(this)
                        function(width, height)
                    }
                }
            }

            viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
            addOnLayoutChangeListener(onLayoutChangeListener)
        } else {
            function(width, height)
        }
    }
}

          private val getWidth: Int
    get() {
        if (Build.VERSION.SDK_INT >= 30) {
            val windowMetrics =windowManager.currentWindowMetrics
            val bounds = windowMetrics.bounds
            var adWidthPixels = View.width.toFloat()
            if (adWidthPixels == 0f) {
                adWidthPixels = bounds.width().toFloat()
            }
            val density = resources.displayMetrics.density
            val adWidth = (adWidthPixels / density).toInt()
            return adWidth
        } else {
            val display = windowManager.defaultDisplay
            val outMetrics = DisplayMetrics()
            display.getMetrics(outMetrics)
            val density = outMetrics.density
            var adWidthPixels = View.width.toFloat()
            if (adWidthPixels == 0f) {
                adWidthPixels = outMetrics.widthPixels.toFloat()
            }
            val adWidth = (adWidthPixels / density).toInt()
            return adWidth
        }
    }

замените (View) представлением, которое вы хотите измерить

      public final class ViewUtils {

    public interface ViewUtilsListener {
        void onDrawCompleted();
    }

    private ViewUtils() {
    }

    public static void onDraw(View view, ViewUtilsListener listener) {
        view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (view.getHeight() != 0 && view.getWidth() != 0) {
                    if (listener != null) {
                        listener.onDrawCompleted();
                    }
                    view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                }
            }
        });
    }
}

вы можете использовать так;

      ViewUtils.onDraw(view, new ViewUtils.ViewUtilsListener() {
                @Override
                public void onDrawCompleted() {
                    int width = view.getWidth();
                    int height = view.getHeight();
                }
            });

В моем случае я не могу получить высоту представления по почте или через addOnGlobalLayoutListener, это всегда 0. Потому что мое представление находится во фрагменте, а фрагмент — это вторая вкладка в MainActivity. когда я открываю MainActivity, я захожу на первую вкладку, поэтому вторая вкладка не отображается на экране. Но функция onGlobalLayout() или post() по-прежнему имеет обратный вызов.

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

После некоторых тестов на Android 7.1.2 я обнаружил, что только эта опция действительно полезна, все остальные вернули 0

    view.postDelayed({
        height = view.height
    },100)

Самое забавное, что я заметил, это то, что результат зависит от времени задержки. Когда я установил delayMillis аргумент до 60 я получил height = 0, с delayMillis = 61 height уже имел значение. Так что я думаю delayMillis = 100 должно быть достаточно.

Если вы беспокоитесь о перегрузке метода onDraw, вы всегда можете установить размер как null во время построения, а затем установить размер внутри onDraw только в том случае, если он равен нулю.

Таким образом, вы на самом деле не делаете никакой работы внутри onDraw.

      class myView(context:Context,attr:AttributeSet?):View(context,attr){
var height:Float?=null


override fun onDraw(canvas:Canvas){
    if (height==null){height=this.height.toFloat()}
    }
}

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

class YourFragment: Fragment() {
    var width = 0
    var height = 0


override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    // Inflate the layout for this fragment
    val root = inflater.inflate(R.layout.fragment_winner_splash, container, false)
    container?.width.let {
        if (it != null) {
            width = it
        }
    }
    container?.height.let {
        if (it != null) {
            height = it
        }
    }
    
    return root
}
 

РЕШЕНИЕ Котлин 2023


  1. Установите параметры макета :

            view.layoutParams = ViewGroup.LayoutParams(0, 0)
    
  2. В методе doOnLayout вы можете получить размер представления:

            view.doOnLayout { view -> view.width | view.height }
    

ПС. Vel_daN: Люби то, что делаешь.


Другие вопросы по тегам