Fix recyclerview jank when using baselayouts

The base layout insets were dispatching after
the screen was already shown, so it was possible
to see the recyclerview behind the toolbar for a
second before it jumped to the correct position.

Don't wait for a global layout to recalculate the
insets, and scroll to the top in setPadding()
instead of waiting for the first global layout.

Fixes: 172575969
Test: Manually and atest CarUILibUnitTests
Change-Id: Ic5fa0b42229a58bbd2934a121d1bbf6762855186
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/BaseLayoutController.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/BaseLayoutController.java
index ed7b5f1..1e5a46c 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/BaseLayoutController.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/BaseLayoutController.java
@@ -23,7 +23,6 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
 import android.widget.FrameLayout;
 
 import androidx.annotation.LayoutRes;
@@ -180,7 +179,6 @@
         }
 
         InsetsUpdater insetsUpdater = new InsetsUpdater(activity, baseLayout, contentView);
-        insetsUpdater.installListeners();
 
         return Pair.create(toolbarController, insetsUpdater);
     }
@@ -208,7 +206,7 @@
      * none of the Activity/Fragments implement {@link InsetsChangedListener}, it will set
      * padding on the content view equal to the insets.
      */
-    static final class InsetsUpdater implements ViewTreeObserver.OnGlobalLayoutListener {
+    static final class InsetsUpdater {
         // These tags mark views that should overlay the content view in the base layout.
         // OEMs should add them to views in their base layout, ie: android:tag="car_ui_left_inset"
         // Apps will then be able to draw under these views, but will be encouraged to not put
@@ -228,7 +226,6 @@
         private final View mBottomInsetView;
         private InsetsChangedListener mInsetsChangedListenerDelegate;
 
-        private boolean mInsetsDirty = true;
         @NonNull
         private Insets mInsets = new Insets();
 
@@ -259,7 +256,7 @@
                             int oldLeft, int oldTop, int oldRight, int oldBottom) -> {
                         if (left != oldLeft || top != oldTop
                                 || right != oldRight || bottom != oldBottom) {
-                            mInsetsDirty = true;
+                            recalcInsets();
                         }
                     };
 
@@ -279,17 +276,6 @@
             mContentViewContainer.addOnLayoutChangeListener(layoutChangeListener);
         }
 
-        /**
-         * Install a global layout listener, during which the insets will be recalculated and
-         * dispatched.
-         */
-        public void installListeners() {
-            // The global layout listener will run after all the individual layout change listeners
-            // so that we only updateInsets once per layout, even if multiple inset views changed
-            mContentView.getRootView().getViewTreeObserver()
-                    .addOnGlobalLayoutListener(this);
-        }
-
         @NonNull
         Insets getInsets() {
             return mInsets;
@@ -300,13 +286,9 @@
         }
 
         /**
-         * onGlobalLayout() should recalculate the amount of insets we need, and then dispatch them.
+         * Recalculate the amount of insets we need, and then dispatch them.
          */
-        @Override
-        public void onGlobalLayout() {
-            if (!mInsetsDirty) {
-                return;
-            }
+        public void recalcInsets() {
 
             // Calculate how much each inset view overlays the content view
 
@@ -339,7 +321,6 @@
             }
             Insets insets = new Insets(left, top, right, bottom);
 
-            mInsetsDirty = false;
             if (!insets.equals(mInsets)) {
                 mInsets = insets;
                 dispatchNewInsets(insets);
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
index 88cf31d..e9e5b3c 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
@@ -26,8 +26,6 @@
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Rect;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.Parcelable;
 import android.text.TextUtils;
 import android.util.AttributeSet;
@@ -38,7 +36,6 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewPropertyAnimator;
-import android.view.ViewTreeObserver;
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 
@@ -79,7 +76,6 @@
     private String mScrollBarClass;
     private int mScrollBarPaddingTop;
     private int mScrollBarPaddingBottom;
-    private boolean mHasScrolledToTop = false;
 
     @Nullable
     private ScrollBar mScrollBar;
@@ -112,18 +108,18 @@
     private int mTopOffset;
     private int mBottomOffset;
 
-    private ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = () -> {
-        if (!mHasScrolledToTop && getLayoutManager() != null) {
-            // Scroll to the top after the first global layout, so that
-            // we can set padding for the insets and still have the
-            // recyclerview start at the top.
-            new Handler(Objects.requireNonNull(Looper.myLooper())).post(() ->
-                    getLayoutManager().scrollToPosition(0));
-            mHasScrolledToTop = true;
+    private boolean mHasScrolled = false;
+
+    private OnScrollListener mOnScrollListener = new OnScrollListener() {
+        @Override
+        public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+            if (dx > 0 || dy > 0) {
+                mHasScrolled = true;
+                removeOnScrollListener(this);
+            }
         }
     };
 
-
     /**
      * The possible values for setScrollBarPosition. The default value is actually {@link
      * CarUiRecyclerViewLayout#LINEAR}.
@@ -244,6 +240,7 @@
         } else if (!isLayoutMangerSet && carUiRecyclerViewLayout == CarUiRecyclerViewLayout.GRID) {
             setLayoutManager(new GridLayoutManager(getContext(), mNumOfColumns));
         }
+        addOnScrollListener(mOnScrollListener);
 
         a.recycle();
 
@@ -342,9 +339,9 @@
     protected void onRestoreInstanceState(Parcelable state) {
         super.onRestoreInstanceState(state);
 
-        // If we're restoring an existing RecyclerView, we don't want
-        // to do the initial scroll to top
-        mHasScrolledToTop = true;
+        // If we're restoring an existing RecyclerView, consider
+        // it as having already scrolled some.
+        mHasScrolled = true;
     }
 
     @Override
@@ -381,7 +378,6 @@
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         mCarUxRestrictionsUtil.register(mListener);
-        this.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
         if (mInstallingExtScrollBar || !mScrollBarEnabled) {
             return;
         }
@@ -472,7 +468,6 @@
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         mCarUxRestrictionsUtil.unregister(mListener);
-        this.getViewTreeObserver().removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
     }
 
     @Override
@@ -480,6 +475,13 @@
         mContainerPaddingRelative = null;
         if (mScrollBarEnabled) {
             super.setPadding(0, top, 0, bottom);
+            if (!mHasScrolled) {
+                // If we haven't scrolled, and thus are still at the top of the screen,
+                // we should stay scrolled to the top after applying padding. Without this
+                // scroll, the padding will start scrolled offscreen. We need the padding
+                // to be onscreen to shift the content into a good visible range.
+                scrollToPosition(0);
+            }
             mContainerPadding = new Rect(left, 0, right, 0);
             if (mContainer != null) {
                 mContainer.setPadding(left, 0, right, 0);
@@ -495,6 +497,13 @@
         mContainerPadding = null;
         if (mScrollBarEnabled) {
             super.setPaddingRelative(0, top, 0, bottom);
+            if (!mHasScrolled) {
+                // If we haven't scrolled, and thus are still at the top of the screen,
+                // we should stay scrolled to the top after applying padding. Without this
+                // scroll, the padding will start scrolled offscreen. We need the padding
+                // to be onscreen to shift the content into a good visible range.
+                scrollToPosition(0);
+            }
             mContainerPaddingRelative = new Rect(start, 0, end, 0);
             if (mContainer != null) {
                 mContainer.setPaddingRelative(start, 0, end, 0);