Loading core/java/android/widget/AbsListView.java +28 −0 Original line number Diff line number Diff line Loading @@ -510,6 +510,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769353) private OnScrollListener mOnScrollListener; /** * Optional callback to notify client when scroll state has changed. This is used internally * to track state changes for Jank metrics. A separate OnScrollListener is used in order to * avoid overwriting any existing OnScrollListener apps may have set. */ @UnsupportedAppUsage private OnScrollListener mOnScrollStateChangeListener; /** * Keeps track of our accessory window */ Loading Loading @@ -653,6 +661,12 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te */ private int mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE; /** * The last scroll state reported to {@link #mOnScrollStateChangeListener}, used for internal * tracking of scroll state changes. */ private int mPreviousOnScrollListenerState = OnScrollListener.SCROLL_STATE_IDLE; /** * Indicates that reporting positions of child views to content capture is enabled via * DeviceConfig. Loading Loading @@ -1609,6 +1623,15 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te onScrollChanged(0, 0, 0, 0); } /** * Set the listener that will receive notifications only when scroll state changes. * * @hide */ protected void setOnScrollStateChangeListener(OnScrollListener listener) { mOnScrollStateChangeListener = listener; } /** * A TYPE_VIEW_SCROLLED event should be sent whenever a scroll happens, even if the * mFirstPosition and the child count have not changed. Loading Loading @@ -4918,6 +4941,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } if (newState != mPreviousOnScrollListenerState && mOnScrollStateChangeListener != null) { mPreviousOnScrollListenerState = newState; mOnScrollStateChangeListener.onScrollStateChanged(this, newState); } // When scrolling, we want to report changes in the active children to Content Capture, // so set the flag to report on the next update only when scrolling has stopped or a fling // scroll is performed. Loading core/java/android/widget/ListView.java +58 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,8 @@ package android.widget; import android.annotation.IdRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.jank.AppJankStats; import android.app.jank.JankTracker; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.Intent; Loading Loading @@ -213,6 +215,12 @@ public class ListView extends AbsListView { // Keeps focused children visible through resizes private FocusSelector mFocusSelector; // Associates scroll state changes to frame counts for jank metric reporting. private JankTracker mJankTracker; // Used to keep track of scroll state transitions for jank metrics. Value of -1 indicates no // previous state has been set. private int mPreviousScrollState = -1; public ListView(Context context) { this(context, null); } Loading Loading @@ -268,6 +276,56 @@ public class ListView extends AbsListView { mFooterDividersEnabled = a.getBoolean(R.styleable.ListView_footerDividersEnabled, true); a.recycle(); if (android.app.jank.Flags.instrumentListviewScrollStates()) { initializeScrollStateTracking(String.valueOf(this.getId())); } } private void initializeScrollStateTracking(String widgetId) { this.setOnScrollStateChangeListener(new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (mJankTracker == null) { mJankTracker = view.getJankTracker(); } // Certain apps are not supported based on their app category. In unsupported apps // JankTracker will always be null. if (mJankTracker != null && scrollState != mPreviousScrollState) { if (mPreviousScrollState == -1) { mJankTracker.addUiState(AppJankStats.WIDGET_CATEGORY_SCROLL, widgetId, getWidgetStateFromScrollState(scrollState)); } else { mJankTracker.updateUiState(AppJankStats.WIDGET_CATEGORY_SCROLL, widgetId, getWidgetStateFromScrollState(mPreviousScrollState), getWidgetStateFromScrollState(scrollState)); } mPreviousScrollState = scrollState; } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { // onScroll is not needed, for jank tracking we are only concerned with state // transitions, not item count and visibility. } }); } private String getWidgetStateFromScrollState(int scrollState) { switch (scrollState) { case OnScrollListener.SCROLL_STATE_FLING -> { return AppJankStats.WIDGET_STATE_FLINGING; } case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL -> { return AppJankStats.WIDGET_STATE_SCROLLING; } default -> { return AppJankStats.WIDGET_STATE_NONE; } } } /** Loading tests/AppJankTest/AndroidManifest.xml +7 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,13 @@ <action android:name="android.intent.action.VIEW_PERMISSION_USAGE"/> </intent-filter> </activity> <activity android:name=".ListViewActivity" android:exported="true" android:label="ListViewActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> </intent-filter> </activity> <uses-library android:name="android.test.runner"/> </application> Loading tests/AppJankTest/res/layout/list_view_activity_layout.xml 0 → 100644 +31 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- ~ Copyright (C) 2025 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. ~ You may obtain a copy of the License at ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless required by applicable law or agreed to in writing, software ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and ~ limitations under the License. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/linear_layout_id" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ListView android:id="@+id/list_view_id" android:layout_width="match_parent" android:layout_height="match_parent" android:contentDescription="TestList" /> </LinearLayout> tests/AppJankTest/res/layout/simple_list_item_1.xml 0 → 100644 +8 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/text1" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:textSize="16sp"/> Loading
core/java/android/widget/AbsListView.java +28 −0 Original line number Diff line number Diff line Loading @@ -510,6 +510,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769353) private OnScrollListener mOnScrollListener; /** * Optional callback to notify client when scroll state has changed. This is used internally * to track state changes for Jank metrics. A separate OnScrollListener is used in order to * avoid overwriting any existing OnScrollListener apps may have set. */ @UnsupportedAppUsage private OnScrollListener mOnScrollStateChangeListener; /** * Keeps track of our accessory window */ Loading Loading @@ -653,6 +661,12 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te */ private int mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE; /** * The last scroll state reported to {@link #mOnScrollStateChangeListener}, used for internal * tracking of scroll state changes. */ private int mPreviousOnScrollListenerState = OnScrollListener.SCROLL_STATE_IDLE; /** * Indicates that reporting positions of child views to content capture is enabled via * DeviceConfig. Loading Loading @@ -1609,6 +1623,15 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te onScrollChanged(0, 0, 0, 0); } /** * Set the listener that will receive notifications only when scroll state changes. * * @hide */ protected void setOnScrollStateChangeListener(OnScrollListener listener) { mOnScrollStateChangeListener = listener; } /** * A TYPE_VIEW_SCROLLED event should be sent whenever a scroll happens, even if the * mFirstPosition and the child count have not changed. Loading Loading @@ -4918,6 +4941,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } if (newState != mPreviousOnScrollListenerState && mOnScrollStateChangeListener != null) { mPreviousOnScrollListenerState = newState; mOnScrollStateChangeListener.onScrollStateChanged(this, newState); } // When scrolling, we want to report changes in the active children to Content Capture, // so set the flag to report on the next update only when scrolling has stopped or a fling // scroll is performed. Loading
core/java/android/widget/ListView.java +58 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,8 @@ package android.widget; import android.annotation.IdRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.jank.AppJankStats; import android.app.jank.JankTracker; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.Intent; Loading Loading @@ -213,6 +215,12 @@ public class ListView extends AbsListView { // Keeps focused children visible through resizes private FocusSelector mFocusSelector; // Associates scroll state changes to frame counts for jank metric reporting. private JankTracker mJankTracker; // Used to keep track of scroll state transitions for jank metrics. Value of -1 indicates no // previous state has been set. private int mPreviousScrollState = -1; public ListView(Context context) { this(context, null); } Loading Loading @@ -268,6 +276,56 @@ public class ListView extends AbsListView { mFooterDividersEnabled = a.getBoolean(R.styleable.ListView_footerDividersEnabled, true); a.recycle(); if (android.app.jank.Flags.instrumentListviewScrollStates()) { initializeScrollStateTracking(String.valueOf(this.getId())); } } private void initializeScrollStateTracking(String widgetId) { this.setOnScrollStateChangeListener(new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (mJankTracker == null) { mJankTracker = view.getJankTracker(); } // Certain apps are not supported based on their app category. In unsupported apps // JankTracker will always be null. if (mJankTracker != null && scrollState != mPreviousScrollState) { if (mPreviousScrollState == -1) { mJankTracker.addUiState(AppJankStats.WIDGET_CATEGORY_SCROLL, widgetId, getWidgetStateFromScrollState(scrollState)); } else { mJankTracker.updateUiState(AppJankStats.WIDGET_CATEGORY_SCROLL, widgetId, getWidgetStateFromScrollState(mPreviousScrollState), getWidgetStateFromScrollState(scrollState)); } mPreviousScrollState = scrollState; } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { // onScroll is not needed, for jank tracking we are only concerned with state // transitions, not item count and visibility. } }); } private String getWidgetStateFromScrollState(int scrollState) { switch (scrollState) { case OnScrollListener.SCROLL_STATE_FLING -> { return AppJankStats.WIDGET_STATE_FLINGING; } case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL -> { return AppJankStats.WIDGET_STATE_SCROLLING; } default -> { return AppJankStats.WIDGET_STATE_NONE; } } } /** Loading
tests/AppJankTest/AndroidManifest.xml +7 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,13 @@ <action android:name="android.intent.action.VIEW_PERMISSION_USAGE"/> </intent-filter> </activity> <activity android:name=".ListViewActivity" android:exported="true" android:label="ListViewActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> </intent-filter> </activity> <uses-library android:name="android.test.runner"/> </application> Loading
tests/AppJankTest/res/layout/list_view_activity_layout.xml 0 → 100644 +31 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- ~ Copyright (C) 2025 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. ~ You may obtain a copy of the License at ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless required by applicable law or agreed to in writing, software ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and ~ limitations under the License. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/linear_layout_id" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ListView android:id="@+id/list_view_id" android:layout_width="match_parent" android:layout_height="match_parent" android:contentDescription="TestList" /> </LinearLayout>
tests/AppJankTest/res/layout/simple_list_item_1.xml 0 → 100644 +8 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/text1" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:textSize="16sp"/>