Почему onLayout и onSizeChanged дважды вызываются при изменении ориентации?

Я заметил, что onLayout и onSizeChanged получают вызовы дважды в непосредственной последовательности после изменения ориентации, будь то пейзаж- > портрет или портрет- > пейзаж, при обработке изменения конфигурации из Activity. Кроме того, первый onLayout/onSizeChanged содержит старые размеры (до поворота), а второй onLayout/onSizeChanged содержит новые (правильные) размеры.

Кто-нибудь знает, почему и/или как обойти это? Похоже, что изменение размера экрана происходит через некоторое время после изменения конфигурации, т.е. Размеры не являются правильными сразу после изменения конфигурации при вызове onConfigurationChanged?

Здесь выводится отладочный вывод кода ниже, показывающий вызовы onLayout/onSizeChanged после поворота от Portrait to Landscape (обратите внимание, что устройство имеет размер 540x960, поэтому ширина ландшафта должна быть 960, а ширина портрета 540):

03-13 17:36:21.140: DEBUG/RotateTest(27765): onConfigurationChanged: LANDSCAPE
03-13 17:36:21.169: DEBUG/RotateTest(27765): onSizeChanged:540,884,0,0
03-13 17:36:21.189: DEBUG/RotateTest(27765): onLayout:true-0,0,540,884
03-13 17:36:21.239: DEBUG/RotateTest(27765): onSizeChanged:960,464,540,884
03-13 17:36:21.259: DEBUG/RotateTest(27765): onLayout:true-0,0,960,464

Обратите внимание также, что первая onSizeChanged oldwidth и oldheight равны 0, что указывает на то, что мы были добавлены в иерархию представлений, но с неправильными размерами для ландшафта!

И вот код, который иллюстрирует это поведение:

MyActivity.java

package com.example;

import android.app.Activity;
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.Log;
import android.widget.FrameLayout;

public class MyActivity extends Activity
{
    private static String TAG = "RotateTest";

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        Log.d(TAG, "onConfigurationChanged: " + (newConfig.orientation == 1 ? "PORTRAIT" : "LANDSCAPE"));
        super.onConfigurationChanged(newConfig);
        _setView();
    }

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        Log.d(TAG, "onCreate");
        super.onCreate(savedInstanceState);
        _setView();
    }

    private void _setView() {
        MyHorizontalScrollView scrollView = new MyHorizontalScrollView(this, null);
        setContentView(scrollView);
    }
}

MyHorizontalScrollView.java

package com.example;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.HorizontalScrollView;

public class MyHorizontalScrollView extends HorizontalScrollView {

    private static String TAG = "RotateTest";

    public MyHorizontalScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        Log.d(TAG, "onLayout:" + String.format("%s-%d,%d,%d,%d", changed, l, t, r, b));
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        Log.d(TAG, "onSizeChanged:" + String.format("%d,%d,%d,%d", w, h, oldw, oldh));
    }
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.example"
      android:versionCode="1"
      android:versionName="1.0"
        >

    <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="9"/>

    <application android:label="@string/app_name"
            >
        <activity android:name="MyActivity"
                  android:label="@string/app_name"
                  android:configChanges="orientation"
                >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest> 

Ответ 1

Мне это очень интересно было очень долго.

Как я ответил на вопрос - потому что я верю, что ответ есть, это зависит от того, добавляет ли оператор try/catch или logging в метод requestLayout. Это позволяет вам видеть, когда сделаны запросы на повторное планирование и повторную укладку, а в случае try/catch, - кем.

Способ, который выкладывается в Android, заключается в том, что вы отмечаете представление как имеющее грязный макет с requestLayout. Анкерный петлеукладчик, который всегда работает на потоке пользовательского интерфейса на некотором интервале, будет повторно отображать и перерисовывать представления в дереве, которые в будущем будут помечены как загрязненные в какой-то неопределенной точке.

Я осмеливаюсь предположить, что onConfigurationChanged вы получаете несколько вызовов requestLayout, а looper вызывает onMeasure где-то посередине.

Это то, что мне понравилось:

11-07 15:39:13.624: W/YARIAN(30006): requestLayout
11-07 15:39:13.632: W/YARIAN(30006): requestLayout
11-07 15:39:13.640: W/YARIAN(30006): requestLayout
11-07 15:39:13.647: W/YARIAN(30006): requestLayout
11-07 15:39:13.686: W/YARIAN(30006): requestLayout
11-07 15:39:13.718: W/YARIAN(30006): requestLayout
11-07 15:39:13.827: W/YARIAN(30006): requestLayout
11-07 15:39:14.108: W/YARIAN(30006): onLayout
11-07 15:39:14.155: W/YARIAN(30006): requestLayout
11-07 15:39:14.272: W/YARIAN(30006): onLayout

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

Обработка событий и потоки

Основной цикл представления выглядит следующим образом:

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

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