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 после выполнения, что может быть хлопотно
- менее подробный синтаксис
Рекомендации:
- /questions/23434865/vyisota-linearlayout-v-oncreate-ravna-0/23434866#23434866
- /questions/48018216/kakoe-sobyitie-zapuskaetsya-posle-togo-kak-vse-predstavleniya-polnostyu-prorisovanyi/48018227#48018227
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
Установите параметры макета :
view.layoutParams = ViewGroup.LayoutParams(0, 0)
В методе doOnLayout вы можете получить размер представления:
view.doOnLayout { view -> view.width | view.height }