diff options
author | 2022-10-05 12:59:07 -0400 | |
---|---|---|
committer | 2022-10-11 17:21:13 +0000 | |
commit | c168f984875ff096720e0a2b092f14c601b5102f (patch) | |
tree | c57ca8ecaad09db0b3ea9d6ee80100a1b4b795c6 | |
parent | 93eadc3993a768cfac005faed2dcf56093cae2d5 (diff) |
Switch off sharing framework's "widgets"
This forks the ResolverDrawerLayout, which appears to be custom and
specific to our app. Any other dependencies from the framework package
com.android.internal.widget were old copies of androidx libraries,
and the "unbundled" IntentResolver is able to take dependencies on the
real versions instead. No dependencies into that framework package
remain after this CL.
These changes were excluded from ag/20001357 (as described in
go/chooser-fork-cl) in order to isolate any possible regressions (and
also to split up the review burden for the original fork CL).
There are no behavior changes *expected* from this CL, but any
edge-case discrepancies are hopefully improvements due to bugfixes or
other refinements that were put into the androidx libraries after the
framework copy was cut. The APIs themselves are supposed to be drop-in
compatible.
Test: atest IntentResolverUnitTests
Bug: 248566504
Change-Id: I8e02fd580d395446066fc2bb239a9bef833ef059
19 files changed, 1295 insertions, 68 deletions
@@ -45,6 +45,8 @@ android_library { static_libs: [ "androidx.annotation_annotation", + "androidx.recyclerview_recyclerview", + "androidx.viewpager_viewpager", "unsupportedappusage", ], diff --git a/java/res/layout/chooser_dialog.xml b/java/res/layout/chooser_dialog.xml index ff66bbb9..e31712c7 100644 --- a/java/res/layout/chooser_dialog.xml +++ b/java/res/layout/chooser_dialog.xml @@ -50,9 +50,9 @@ </LinearLayout> - <com.android.internal.widget.RecyclerView + <androidx.recyclerview.widget.RecyclerView xmlns:app="http://schemas.android.com/apk/res-auto" - androidprv:layoutManager="com.android.internal.widget.LinearLayoutManager" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" android:id="@androidprv:id/listContainer" android:overScrollMode="never" android:layout_width="match_parent" diff --git a/java/res/layout/chooser_grid.xml b/java/res/layout/chooser_grid.xml index a95b0ebe..d863495d 100644 --- a/java/res/layout/chooser_grid.xml +++ b/java/res/layout/chooser_grid.xml @@ -16,14 +16,15 @@ * limitations under the License. */ --> -<com.android.internal.widget.ResolverDrawerLayout +<com.android.intentresolver.widget.ResolverDrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" - androidprv:maxCollapsedHeight="0dp" - androidprv:maxCollapsedHeightSmall="56dp" + app:maxCollapsedHeight="0dp" + app:maxCollapsedHeightSmall="56dp" android:maxWidth="@dimen/chooser_width" android:id="@androidprv:id/contentPanel"> @@ -31,7 +32,7 @@ android:id="@androidprv:id/chooser_header" android:layout_width="match_parent" android:layout_height="wrap_content" - androidprv:layout_alwaysShow="true" + app:layout_alwaysShow="true" android:elevation="0dp" android:background="@drawable/bottomsheet_background"> @@ -94,4 +95,4 @@ </LinearLayout> </TabHost> -</com.android.internal.widget.ResolverDrawerLayout> +</com.android.intentresolver.widget.ResolverDrawerLayout> diff --git a/java/res/layout/chooser_list_per_profile.xml b/java/res/layout/chooser_list_per_profile.xml index 8d876cdf..1753e2f6 100644 --- a/java/res/layout/chooser_list_per_profile.xml +++ b/java/res/layout/chooser_list_per_profile.xml @@ -16,12 +16,13 @@ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> - <com.android.internal.widget.RecyclerView + <androidx.recyclerview.widget.RecyclerView android:layout_width="match_parent" android:layout_height="match_parent" - androidprv:layoutManager="com.android.intentresolver.ChooserGridLayoutManager" + app:layoutManager="com.android.intentresolver.ChooserGridLayoutManager" android:id="@androidprv:id/resolver_list" android:clipToPadding="false" android:background="?android:attr/colorBackground" diff --git a/java/res/layout/miniresolver.xml b/java/res/layout/miniresolver.xml index ab65aa9b..7e31de57 100644 --- a/java/res/layout/miniresolver.xml +++ b/java/res/layout/miniresolver.xml @@ -14,20 +14,21 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<com.android.internal.widget.ResolverDrawerLayout +<com.android.intentresolver.widget.ResolverDrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:maxWidth="@dimen/resolver_max_width" - androidprv:maxCollapsedHeight="@dimen/resolver_max_collapsed_height" - androidprv:maxCollapsedHeightSmall="56dp" + app:maxCollapsedHeight="@dimen/resolver_max_collapsed_height" + app:maxCollapsedHeightSmall="56dp" android:id="@androidprv:id/contentPanel"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" - androidprv:layout_alwaysShow="true" + app:layout_alwaysShow="true" android:elevation="@dimen/resolver_elevation" android:paddingTop="24dp" android:paddingStart="@dimen/resolver_edge_margin" @@ -62,18 +63,18 @@ android:id="@androidprv:id/button_bar_container" android:layout_width="match_parent" android:layout_height="wrap_content" - androidprv:layout_alwaysShow="true" + app:layout_alwaysShow="true" android:paddingTop="32dp" android:paddingBottom="@dimen/resolver_button_bar_spacing" android:orientation="vertical" android:background="?android:attr/colorBackground" - androidprv:layout_ignoreOffset="true"> + app:layout_ignoreOffset="true"> <RelativeLayout style="?android:attr/buttonBarStyle" android:layout_width="match_parent" android:layout_height="wrap_content" - androidprv:layout_ignoreOffset="true" - androidprv:layout_hasNestedScrollIndicator="true" + app:layout_ignoreOffset="true" + app:layout_hasNestedScrollIndicator="true" android:gravity="end|center_vertical" android:orientation="horizontal" android:layoutDirection="locale" @@ -112,4 +113,4 @@ /> </RelativeLayout> </LinearLayout> -</com.android.internal.widget.ResolverDrawerLayout> +</com.android.intentresolver.widget.ResolverDrawerLayout> diff --git a/java/res/layout/resolver_different_item_header.xml b/java/res/layout/resolver_different_item_header.xml index 4f801597..79ce6824 100644 --- a/java/res/layout/resolver_different_item_header.xml +++ b/java/res/layout/resolver_different_item_header.xml @@ -19,9 +19,10 @@ <TextView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" - androidprv:layout_alwaysShow="true" + app:layout_alwaysShow="true" android:text="@string/use_a_different_app" android:textColor="?android:attr/textColorPrimary" android:fontFamily="@androidprv:string/config_headlineFontFamilyMedium" diff --git a/java/res/layout/resolver_list.xml b/java/res/layout/resolver_list.xml index 179c4073..44b14baf 100644 --- a/java/res/layout/resolver_list.xml +++ b/java/res/layout/resolver_list.xml @@ -16,21 +16,22 @@ * limitations under the License. */ --> -<com.android.internal.widget.ResolverDrawerLayout +<com.android.intentresolver.widget.ResolverDrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:maxWidth="@dimen/resolver_max_width" - androidprv:maxCollapsedHeight="@dimen/resolver_max_collapsed_height" - androidprv:maxCollapsedHeightSmall="56dp" + app:maxCollapsedHeight="@dimen/resolver_max_collapsed_height" + app:maxCollapsedHeightSmall="56dp" android:id="@androidprv:id/contentPanel"> <RelativeLayout android:id="@androidprv:id/title_container" android:layout_width="match_parent" android:layout_height="wrap_content" - androidprv:layout_alwaysShow="true" + app:layout_alwaysShow="true" android:elevation="@dimen/resolver_elevation" android:paddingTop="@dimen/resolver_small_margin" android:paddingStart="@dimen/resolver_edge_margin" @@ -66,7 +67,7 @@ <View android:id="@androidprv:id/divider" - androidprv:layout_alwaysShow="true" + app:layout_alwaysShow="true" android:layout_width="match_parent" android:layout_height="1dp" android:background="?android:attr/colorBackground" @@ -114,10 +115,10 @@ android:id="@androidprv:id/button_bar_container" android:layout_width="match_parent" android:layout_height="wrap_content" - androidprv:layout_alwaysShow="true" + app:layout_alwaysShow="true" android:orientation="vertical" android:background="?android:attr/colorBackground" - androidprv:layout_ignoreOffset="true"> + app:layout_ignoreOffset="true"> <View android:id="@androidprv:id/resolver_button_bar_divider" android:layout_width="match_parent" @@ -130,8 +131,8 @@ style="?android:attr/buttonBarStyle" android:layout_width="match_parent" android:layout_height="wrap_content" - androidprv:layout_ignoreOffset="true" - androidprv:layout_hasNestedScrollIndicator="true" + app:layout_ignoreOffset="true" + app:layout_hasNestedScrollIndicator="true" android:gravity="end|center_vertical" android:orientation="horizontal" android:layoutDirection="locale" @@ -169,4 +170,4 @@ android:onClick="onButtonClick" /> </LinearLayout> </LinearLayout> -</com.android.internal.widget.ResolverDrawerLayout> +</com.android.intentresolver.widget.ResolverDrawerLayout> diff --git a/java/res/layout/resolver_list_with_default.xml b/java/res/layout/resolver_list_with_default.xml index 341c58e7..192a5983 100644 --- a/java/res/layout/resolver_list_with_default.xml +++ b/java/res/layout/resolver_list_with_default.xml @@ -16,19 +16,20 @@ * limitations under the License. */ --> -<com.android.internal.widget.ResolverDrawerLayout +<com.android.intentresolver.widget.ResolverDrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:maxWidth="@dimen/resolver_max_width" - androidprv:maxCollapsedHeight="@dimen/resolver_max_collapsed_height_with_default" + app:maxCollapsedHeight="@dimen/resolver_max_collapsed_height_with_default" android:id="@androidprv:id/contentPanel"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - androidprv:layout_alwaysShow="true" + app:layout_alwaysShow="true" android:orientation="vertical" android:background="@drawable/bottomsheet_background" android:paddingTop="@dimen/resolver_small_margin" @@ -105,7 +106,7 @@ style="?android:attr/buttonBarStyle" android:layout_width="match_parent" android:layout_height="wrap_content" - androidprv:layout_alwaysShow="true" + app:layout_alwaysShow="true" android:gravity="end|center_vertical" android:orientation="horizontal" android:layoutDirection="locale" @@ -146,7 +147,7 @@ <View android:id="@androidprv:id/divider" - androidprv:layout_alwaysShow="true" + app:layout_alwaysShow="true" android:layout_width="match_parent" android:layout_height="1dp" android:background="?android:attr/colorBackground" @@ -154,14 +155,14 @@ <FrameLayout android:id="@androidprv:id/stub" - androidprv:layout_alwaysShow="true" + app:layout_alwaysShow="true" android:visibility="gone" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?android:attr/colorBackground"/> <TabHost - androidprv:layout_alwaysShow="true" + app:layout_alwaysShow="true" android:id="@androidprv:id/profile_tabhost" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -198,9 +199,9 @@ </TabHost> <View - androidprv:layout_alwaysShow="true" + app:layout_alwaysShow="true" android:layout_width="match_parent" android:layout_height="1dp" android:background="?android:attr/colorBackground" android:foreground="?android:attr/dividerVertical" /> -</com.android.internal.widget.ResolverDrawerLayout> +</com.android.intentresolver.widget.ResolverDrawerLayout> diff --git a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java index 4f6c0bf1..0f0d1797 100644 --- a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java @@ -34,9 +34,10 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.widget.PagerAdapter; -import com.android.internal.widget.ViewPager; import java.util.HashSet; import java.util.List; diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index d0e47562..36b32f6a 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -112,6 +112,10 @@ import android.widget.ImageView; import android.widget.Space; import android.widget.TextView; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager.widget.ViewPager; + import com.android.intentresolver.ResolverListAdapter.ActivityInfoPresentationGetter; import com.android.intentresolver.ResolverListAdapter.ViewHolder; import com.android.intentresolver.chooser.ChooserTargetInfo; @@ -121,16 +125,13 @@ import com.android.intentresolver.chooser.NotSelectableTargetInfo; import com.android.intentresolver.chooser.SelectableTargetInfo; import com.android.intentresolver.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.content.PackageMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.FrameworkStatsLog; -import com.android.internal.widget.GridLayoutManager; -import com.android.internal.widget.RecyclerView; -import com.android.internal.widget.ResolverDrawerLayout; -import com.android.internal.widget.ViewPager; import com.google.android.collect.Lists; diff --git a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java index 7c4b0c1f..5f373525 100644 --- a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java +++ b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java @@ -19,8 +19,8 @@ package com.android.intentresolver; import android.content.Context; import android.util.AttributeSet; -import com.android.internal.widget.GridLayoutManager; -import com.android.internal.widget.RecyclerView; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; /** * For a11y and per {@link RecyclerView#onInitializeAccessibilityNodeInfo}, override diff --git a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java index da78fc81..62c14866 100644 --- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java @@ -33,10 +33,11 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager.widget.PagerAdapter; + import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.widget.GridLayoutManager; -import com.android.internal.widget.PagerAdapter; -import com.android.internal.widget.RecyclerView; /** * A {@link PagerAdapter} which describes the work and personal profile share sheet screens. diff --git a/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java index 67571b44..250b6827 100644 --- a/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java +++ b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java @@ -22,8 +22,8 @@ import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; -import com.android.internal.widget.RecyclerView; -import com.android.internal.widget.RecyclerViewAccessibilityDelegate; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; class ChooserRecyclerViewAccessibilityDelegate extends RecyclerViewAccessibilityDelegate { private final Rect mTempRect = new Rect(); diff --git a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java index ffd173c7..b4a102ae 100644 --- a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java +++ b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java @@ -49,9 +49,9 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import com.android.intentresolver.chooser.DisplayResolveInfo; +import androidx.recyclerview.widget.RecyclerView; -import com.android.internal.widget.RecyclerView; +import com.android.intentresolver.chooser.DisplayResolveInfo; import java.util.ArrayList; import java.util.List; diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java index 6c013221..ea140dcb 100644 --- a/java/src/com/android/intentresolver/ResolverActivity.java +++ b/java/src/com/android/intentresolver/ResolverActivity.java @@ -91,17 +91,18 @@ import android.widget.TabWidget; import android.widget.TextView; import android.widget.Toast; +import androidx.viewpager.widget.ViewPager; + import com.android.intentresolver.AbstractMultiProfilePagerAdapter.Profile; import com.android.intentresolver.chooser.ChooserTargetInfo; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; import com.android.internal.util.LatencyTracker; -import com.android.internal.widget.ResolverDrawerLayout; -import com.android.internal.widget.ViewPager; import java.util.ArrayList; import java.util.Arrays; diff --git a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java index 56d326c1..7cd38a7e 100644 --- a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java @@ -32,8 +32,9 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ListView; +import androidx.viewpager.widget.PagerAdapter; + import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.widget.PagerAdapter; /** * A {@link PagerAdapter} which describes the work and personal profile intent resolver screens. diff --git a/java/src/com/android/intentresolver/ResolverViewPager.java b/java/src/com/android/intentresolver/ResolverViewPager.java index 1c234526..0804a2b8 100644 --- a/java/src/com/android/intentresolver/ResolverViewPager.java +++ b/java/src/com/android/intentresolver/ResolverViewPager.java @@ -21,7 +21,7 @@ import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; -import com.android.internal.widget.ViewPager; +import androidx.viewpager.widget.ViewPager; /** * A {@link ViewPager} which wraps around its tallest child's height. @@ -41,15 +41,6 @@ public class ResolverViewPager extends ViewPager { super(context, attrs); } - public ResolverViewPager(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public ResolverViewPager(Context context, AttributeSet attrs, - int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); diff --git a/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java new file mode 100644 index 00000000..29821e66 --- /dev/null +++ b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java @@ -0,0 +1,1223 @@ +/* + * Copyright (C) 2014 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. + */ + + +package com.android.intentresolver.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.metrics.LogMaker; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.ViewTreeObserver; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; +import android.view.animation.AnimationUtils; +import android.widget.AbsListView; +import android.widget.OverScroller; + +import androidx.recyclerview.widget.RecyclerView; + +import com.android.intentresolver.R; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; + +public class ResolverDrawerLayout extends ViewGroup { + private static final String TAG = "ResolverDrawerLayout"; + private MetricsLogger mMetricsLogger; + + /** + * Max width of the whole drawer layout + */ + private final int mMaxWidth; + + /** + * Max total visible height of views not marked always-show when in the closed/initial state + */ + private int mMaxCollapsedHeight; + + /** + * Max total visible height of views not marked always-show when in the closed/initial state + * when a default option is present + */ + private int mMaxCollapsedHeightSmall; + + /** + * Whether {@code mMaxCollapsedHeightSmall} was set explicitly as a layout attribute or + * inferred by {@code mMaxCollapsedHeight}. + */ + private final boolean mIsMaxCollapsedHeightSmallExplicit; + + private boolean mSmallCollapsed; + + /** + * Move views down from the top by this much in px + */ + private float mCollapseOffset; + + /** + * Track fractions of pixels from drag calculations. Without this, the view offsets get + * out of sync due to frequently dropping fractions of a pixel from '(int) dy' casts. + */ + private float mDragRemainder = 0.0f; + private int mCollapsibleHeight; + private int mUncollapsibleHeight; + private int mAlwaysShowHeight; + + /** + * The height in pixels of reserved space added to the top of the collapsed UI; + * e.g. chooser targets + */ + private int mCollapsibleHeightReserved; + + private int mTopOffset; + private boolean mShowAtTop; + + private boolean mIsDragging; + private boolean mOpenOnClick; + private boolean mOpenOnLayout; + private boolean mDismissOnScrollerFinished; + private final int mTouchSlop; + private final float mMinFlingVelocity; + private final OverScroller mScroller; + private final VelocityTracker mVelocityTracker; + + private Drawable mScrollIndicatorDrawable; + + private OnDismissedListener mOnDismissedListener; + private RunOnDismissedListener mRunOnDismissedListener; + private OnCollapsedChangedListener mOnCollapsedChangedListener; + + private boolean mDismissLocked; + + private float mInitialTouchX; + private float mInitialTouchY; + private float mLastTouchY; + private int mActivePointerId = MotionEvent.INVALID_POINTER_ID; + + private final Rect mTempRect = new Rect(); + + private AbsListView mNestedListChild; + private RecyclerView mNestedRecyclerChild; + + private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener = + new ViewTreeObserver.OnTouchModeChangeListener() { + @Override + public void onTouchModeChanged(boolean isInTouchMode) { + if (!isInTouchMode && hasFocus() && isDescendantClipped(getFocusedChild())) { + smoothScrollTo(0, 0); + } + } + }; + + public ResolverDrawerLayout(Context context) { + this(context, null); + } + + public ResolverDrawerLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout, + defStyleAttr, 0); + mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_android_maxWidth, -1); + mMaxCollapsedHeight = a.getDimensionPixelSize( + R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0); + mMaxCollapsedHeightSmall = a.getDimensionPixelSize( + R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall, + mMaxCollapsedHeight); + mIsMaxCollapsedHeightSmallExplicit = + a.hasValue(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall); + mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false); + a.recycle(); + + mScrollIndicatorDrawable = mContext.getDrawable( + com.android.internal.R.drawable.scroll_indicator_material); + + mScroller = new OverScroller(context, AnimationUtils.loadInterpolator(context, + android.R.interpolator.decelerate_quint)); + mVelocityTracker = VelocityTracker.obtain(); + + final ViewConfiguration vc = ViewConfiguration.get(context); + mTouchSlop = vc.getScaledTouchSlop(); + mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); + + setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + + /** + * Dynamically set the max collapsed height. Note this also updates the small collapsed + * height if it wasn't specified explicitly. + */ + public void setMaxCollapsedHeight(int heightInPixels) { + if (heightInPixels == mMaxCollapsedHeight) { + return; + } + mMaxCollapsedHeight = heightInPixels; + if (!mIsMaxCollapsedHeightSmallExplicit) { + mMaxCollapsedHeightSmall = mMaxCollapsedHeight; + } + requestLayout(); + } + + public void setSmallCollapsed(boolean smallCollapsed) { + if (mSmallCollapsed != smallCollapsed) { + mSmallCollapsed = smallCollapsed; + requestLayout(); + } + } + + public boolean isSmallCollapsed() { + return mSmallCollapsed; + } + + public boolean isCollapsed() { + return mCollapseOffset > 0; + } + + public void setShowAtTop(boolean showOnTop) { + if (mShowAtTop != showOnTop) { + mShowAtTop = showOnTop; + requestLayout(); + } + } + + public boolean getShowAtTop() { + return mShowAtTop; + } + + public void setCollapsed(boolean collapsed) { + if (!isLaidOut()) { + mOpenOnLayout = !collapsed; + } else { + smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0); + } + } + + public void setCollapsibleHeightReserved(int heightPixels) { + final int oldReserved = mCollapsibleHeightReserved; + mCollapsibleHeightReserved = heightPixels; + if (oldReserved != mCollapsibleHeightReserved) { + requestLayout(); + } + + final int dReserved = mCollapsibleHeightReserved - oldReserved; + if (dReserved != 0 && mIsDragging) { + mLastTouchY -= dReserved; + } + + final int oldCollapsibleHeight = mCollapsibleHeight; + mCollapsibleHeight = Math.min(mCollapsibleHeight, getMaxCollapsedHeight()); + + if (updateCollapseOffset(oldCollapsibleHeight, !isDragging())) { + return; + } + + invalidate(); + } + + public void setDismissLocked(boolean locked) { + mDismissLocked = locked; + } + + private boolean isMoving() { + return mIsDragging || !mScroller.isFinished(); + } + + private boolean isDragging() { + return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL; + } + + private boolean updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed) { + if (oldCollapsibleHeight == mCollapsibleHeight) { + return false; + } + + if (getShowAtTop()) { + // Keep the drawer fully open. + setCollapseOffset(0); + return false; + } + + if (isLaidOut()) { + final boolean isCollapsedOld = mCollapseOffset != 0; + if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight + && mCollapseOffset == oldCollapsibleHeight)) { + // Stay closed even at the new height. + setCollapseOffset(mCollapsibleHeight); + } else { + setCollapseOffset(Math.min(mCollapseOffset, mCollapsibleHeight)); + } + final boolean isCollapsedNew = mCollapseOffset != 0; + if (isCollapsedOld != isCollapsedNew) { + onCollapsedChanged(isCollapsedNew); + } + } else { + // Start out collapsed at first unless we restored state for otherwise + setCollapseOffset(mOpenOnLayout ? 0 : mCollapsibleHeight); + } + return true; + } + + private void setCollapseOffset(float collapseOffset) { + if (mCollapseOffset != collapseOffset) { + mCollapseOffset = collapseOffset; + requestLayout(); + } + } + + private int getMaxCollapsedHeight() { + return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight) + + mCollapsibleHeightReserved; + } + + public void setOnDismissedListener(OnDismissedListener listener) { + mOnDismissedListener = listener; + } + + private boolean isDismissable() { + return mOnDismissedListener != null && !mDismissLocked; + } + + public void setOnCollapsedChangedListener(OnCollapsedChangedListener listener) { + mOnCollapsedChangedListener = listener; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + final int action = ev.getActionMasked(); + + if (action == MotionEvent.ACTION_DOWN) { + mVelocityTracker.clear(); + } + + mVelocityTracker.addMovement(ev); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + mInitialTouchX = x; + mInitialTouchY = mLastTouchY = y; + mOpenOnClick = isListChildUnderClipped(x, y) && mCollapseOffset > 0; + } + break; + + case MotionEvent.ACTION_MOVE: { + final float x = ev.getX(); + final float y = ev.getY(); + final float dy = y - mInitialTouchY; + if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null && + (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) { + mActivePointerId = ev.getPointerId(0); + mIsDragging = true; + mLastTouchY = Math.max(mLastTouchY - mTouchSlop, + Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop)); + } + } + break; + + case MotionEvent.ACTION_POINTER_UP: { + onSecondaryPointerUp(ev); + } + break; + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + resetTouch(); + } + break; + } + + if (mIsDragging) { + abortAnimation(); + } + return mIsDragging || mOpenOnClick; + } + + private boolean isNestedListChildScrolled() { + return mNestedListChild != null + && mNestedListChild.getChildCount() > 0 + && (mNestedListChild.getFirstVisiblePosition() > 0 + || mNestedListChild.getChildAt(0).getTop() < 0); + } + + private boolean isNestedRecyclerChildScrolled() { + if (mNestedRecyclerChild != null && mNestedRecyclerChild.getChildCount() > 0) { + final RecyclerView.ViewHolder vh = + mNestedRecyclerChild.findViewHolderForAdapterPosition(0); + return vh == null || vh.itemView.getTop() < 0; + } + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + final int action = ev.getActionMasked(); + + mVelocityTracker.addMovement(ev); + + boolean handled = false; + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + mInitialTouchX = x; + mInitialTouchY = mLastTouchY = y; + mActivePointerId = ev.getPointerId(0); + final boolean hitView = findChildUnder(mInitialTouchX, mInitialTouchY) != null; + handled = isDismissable() || mCollapsibleHeight > 0; + mIsDragging = hitView && handled; + abortAnimation(); + } + break; + + case MotionEvent.ACTION_MOVE: { + int index = ev.findPointerIndex(mActivePointerId); + if (index < 0) { + Log.e(TAG, "Bad pointer id " + mActivePointerId + ", resetting"); + index = 0; + mActivePointerId = ev.getPointerId(0); + mInitialTouchX = ev.getX(); + mInitialTouchY = mLastTouchY = ev.getY(); + } + final float x = ev.getX(index); + final float y = ev.getY(index); + if (!mIsDragging) { + final float dy = y - mInitialTouchY; + if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null) { + handled = mIsDragging = true; + mLastTouchY = Math.max(mLastTouchY - mTouchSlop, + Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop)); + } + } + if (mIsDragging) { + final float dy = y - mLastTouchY; + if (dy > 0 && isNestedListChildScrolled()) { + mNestedListChild.smoothScrollBy((int) -dy, 0); + } else if (dy > 0 && isNestedRecyclerChildScrolled()) { + mNestedRecyclerChild.scrollBy(0, (int) -dy); + } else { + performDrag(dy); + } + } + mLastTouchY = y; + } + break; + + case MotionEvent.ACTION_POINTER_DOWN: { + final int pointerIndex = ev.getActionIndex(); + mActivePointerId = ev.getPointerId(pointerIndex); + mInitialTouchX = ev.getX(pointerIndex); + mInitialTouchY = mLastTouchY = ev.getY(pointerIndex); + } + break; + + case MotionEvent.ACTION_POINTER_UP: { + onSecondaryPointerUp(ev); + } + break; + + case MotionEvent.ACTION_UP: { + final boolean wasDragging = mIsDragging; + mIsDragging = false; + if (!wasDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null && + findChildUnder(ev.getX(), ev.getY()) == null) { + if (isDismissable()) { + dispatchOnDismissed(); + resetTouch(); + return true; + } + } + if (mOpenOnClick && Math.abs(ev.getX() - mInitialTouchX) < mTouchSlop && + Math.abs(ev.getY() - mInitialTouchY) < mTouchSlop) { + smoothScrollTo(0, 0); + return true; + } + mVelocityTracker.computeCurrentVelocity(1000); + final float yvel = mVelocityTracker.getYVelocity(mActivePointerId); + if (Math.abs(yvel) > mMinFlingVelocity) { + if (getShowAtTop()) { + if (isDismissable() && yvel < 0) { + abortAnimation(); + dismiss(); + } else { + smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel); + } + } else { + if (isDismissable() + && yvel > 0 && mCollapseOffset > mCollapsibleHeight) { + smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, yvel); + mDismissOnScrollerFinished = true; + } else { + scrollNestedScrollableChildBackToTop(); + smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel); + } + } + }else { + smoothScrollTo( + mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); + } + resetTouch(); + } + break; + + case MotionEvent.ACTION_CANCEL: { + if (mIsDragging) { + smoothScrollTo( + mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); + } + resetTouch(); + return true; + } + } + + return handled; + } + + /** + * Scroll nested scrollable child back to top if it has been scrolled. + */ + public void scrollNestedScrollableChildBackToTop() { + if (isNestedListChildScrolled()) { + mNestedListChild.smoothScrollToPosition(0); + } else if (isNestedRecyclerChildScrolled()) { + mNestedRecyclerChild.smoothScrollToPosition(0); + } + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = ev.getActionIndex(); + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mInitialTouchX = ev.getX(newPointerIndex); + mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex); + mActivePointerId = ev.getPointerId(newPointerIndex); + } + } + + private void resetTouch() { + mActivePointerId = MotionEvent.INVALID_POINTER_ID; + mIsDragging = false; + mOpenOnClick = false; + mInitialTouchX = mInitialTouchY = mLastTouchY = 0; + mVelocityTracker.clear(); + } + + private void dismiss() { + mRunOnDismissedListener = new RunOnDismissedListener(); + post(mRunOnDismissedListener); + } + + @Override + public void computeScroll() { + super.computeScroll(); + if (mScroller.computeScrollOffset()) { + final boolean keepGoing = !mScroller.isFinished(); + performDrag(mScroller.getCurrY() - mCollapseOffset); + if (keepGoing) { + postInvalidateOnAnimation(); + } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) { + dismiss(); + } + } + } + + private void abortAnimation() { + mScroller.abortAnimation(); + mRunOnDismissedListener = null; + mDismissOnScrollerFinished = false; + } + + private float performDrag(float dy) { + if (getShowAtTop()) { + return 0; + } + + final float newPos = Math.max(0, Math.min(mCollapseOffset + dy, + mCollapsibleHeight + mUncollapsibleHeight)); + if (newPos != mCollapseOffset) { + dy = newPos - mCollapseOffset; + + mDragRemainder += dy - (int) dy; + if (mDragRemainder >= 1.0f) { + mDragRemainder -= 1.0f; + dy += 1.0f; + } else if (mDragRemainder <= -1.0f) { + mDragRemainder += 1.0f; + dy -= 1.0f; + } + + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.ignoreOffset) { + child.offsetTopAndBottom((int) dy); + } + } + final boolean isCollapsedOld = mCollapseOffset != 0; + mCollapseOffset = newPos; + mTopOffset += dy; + final boolean isCollapsedNew = newPos != 0; + if (isCollapsedOld != isCollapsedNew) { + onCollapsedChanged(isCollapsedNew); + getMetricsLogger().write( + new LogMaker(MetricsEvent.ACTION_SHARESHEET_COLLAPSED_CHANGED) + .setSubtype(isCollapsedNew ? 1 : 0)); + } + onScrollChanged(0, (int) newPos, 0, (int) (newPos - dy)); + postInvalidateOnAnimation(); + return dy; + } + return 0; + } + + private void onCollapsedChanged(boolean isCollapsed) { + notifyViewAccessibilityStateChangedIfNeeded( + AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); + + if (mScrollIndicatorDrawable != null) { + setWillNotDraw(!isCollapsed); + } + + if (mOnCollapsedChangedListener != null) { + mOnCollapsedChangedListener.onCollapsedChanged(isCollapsed); + } + } + + void dispatchOnDismissed() { + if (mOnDismissedListener != null) { + mOnDismissedListener.onDismissed(); + } + if (mRunOnDismissedListener != null) { + removeCallbacks(mRunOnDismissedListener); + mRunOnDismissedListener = null; + } + } + + private void smoothScrollTo(int yOffset, float velocity) { + abortAnimation(); + final int sy = (int) mCollapseOffset; + int dy = yOffset - sy; + if (dy == 0) { + return; + } + + final int height = getHeight(); + final int halfHeight = height / 2; + final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / height); + final float distance = halfHeight + halfHeight * + distanceInfluenceForSnapDuration(distanceRatio); + + int duration = 0; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + final float pageDelta = (float) Math.abs(dy) / height; + duration = (int) ((pageDelta + 1) * 100); + } + duration = Math.min(duration, 300); + + mScroller.startScroll(0, sy, 0, dy, duration); + postInvalidateOnAnimation(); + } + + private float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + /** + * Note: this method doesn't take Z into account for overlapping views + * since it is only used in contexts where this doesn't affect the outcome. + */ + private View findChildUnder(float x, float y) { + return findChildUnder(this, x, y); + } + + private static View findChildUnder(ViewGroup parent, float x, float y) { + final int childCount = parent.getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + final View child = parent.getChildAt(i); + if (isChildUnder(child, x, y)) { + return child; + } + } + return null; + } + + private View findListChildUnder(float x, float y) { + View v = findChildUnder(x, y); + while (v != null) { + x -= v.getX(); + y -= v.getY(); + if (v instanceof AbsListView) { + // One more after this. + return findChildUnder((ViewGroup) v, x, y); + } + v = v instanceof ViewGroup ? findChildUnder((ViewGroup) v, x, y) : null; + } + return v; + } + + /** + * This only checks clipping along the bottom edge. + */ + private boolean isListChildUnderClipped(float x, float y) { + final View listChild = findListChildUnder(x, y); + return listChild != null && isDescendantClipped(listChild); + } + + private boolean isDescendantClipped(View child) { + mTempRect.set(0, 0, child.getWidth(), child.getHeight()); + offsetDescendantRectToMyCoords(child, mTempRect); + View directChild; + if (child.getParent() == this) { + directChild = child; + } else { + View v = child; + ViewParent p = child.getParent(); + while (p != this) { + v = (View) p; + p = v.getParent(); + } + directChild = v; + } + + // ResolverDrawerLayout lays out vertically in child order; + // the next view and forward is what to check against. + int clipEdge = getHeight() - getPaddingBottom(); + final int childCount = getChildCount(); + for (int i = indexOfChild(directChild) + 1; i < childCount; i++) { + final View nextChild = getChildAt(i); + if (nextChild.getVisibility() == GONE) { + continue; + } + clipEdge = Math.min(clipEdge, nextChild.getTop()); + } + return mTempRect.bottom > clipEdge; + } + + private static boolean isChildUnder(View child, float x, float y) { + final float left = child.getX(); + final float top = child.getY(); + final float right = left + child.getWidth(); + final float bottom = top + child.getHeight(); + return x >= left && y >= top && x < right && y < bottom; + } + + @Override + public void requestChildFocus(View child, View focused) { + super.requestChildFocus(child, focused); + if (!isInTouchMode() && isDescendantClipped(focused)) { + smoothScrollTo(0, 0); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + getViewTreeObserver().addOnTouchModeChangeListener(mTouchModeChangeListener); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + getViewTreeObserver().removeOnTouchModeChangeListener(mTouchModeChangeListener); + abortAnimation(); + } + + @Override + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { + if ((nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0) { + if (target instanceof AbsListView) { + mNestedListChild = (AbsListView) target; + } + if (target instanceof RecyclerView) { + mNestedRecyclerChild = (RecyclerView) target; + } + return true; + } + return false; + } + + @Override + public void onNestedScrollAccepted(View child, View target, int axes) { + super.onNestedScrollAccepted(child, target, axes); + } + + @Override + public void onStopNestedScroll(View child) { + super.onStopNestedScroll(child); + if (mScroller.isFinished()) { + smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0); + } + } + + @Override + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed) { + if (dyUnconsumed < 0) { + performDrag(-dyUnconsumed); + } + } + + @Override + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { + if (dy > 0) { + consumed[1] = (int) -performDrag(-dy); + } + } + + @Override + public boolean onNestedPreFling(View target, float velocityX, float velocityY) { + if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) { + smoothScrollTo(0, velocityY); + return true; + } + return false; + } + + @Override + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) { + if (getShowAtTop()) { + if (isDismissable() && velocityY > 0) { + abortAnimation(); + dismiss(); + } else { + smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY); + } + } else { + if (isDismissable() + && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) { + smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, velocityY); + mDismissOnScrollerFinished = true; + } else { + smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY); + } + } + return true; + } + return false; + } + + private boolean performAccessibilityActionCommon(int action) { + switch (action) { + case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: + case AccessibilityNodeInfo.ACTION_EXPAND: + case com.android.internal.R.id.accessibilityActionScrollDown: + if (mCollapseOffset != 0) { + smoothScrollTo(0, 0); + return true; + } + break; + case AccessibilityNodeInfo.ACTION_COLLAPSE: + if (mCollapseOffset < mCollapsibleHeight) { + smoothScrollTo(mCollapsibleHeight, 0); + return true; + } + break; + case AccessibilityNodeInfo.ACTION_DISMISS: + if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight) + && isDismissable()) { + smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, 0); + mDismissOnScrollerFinished = true; + return true; + } + break; + } + + return false; + } + + @Override + public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) { + if (super.onNestedPrePerformAccessibilityAction(target, action, args)) { + return true; + } + + return performAccessibilityActionCommon(action); + } + + @Override + public CharSequence getAccessibilityClassName() { + // Since we support scrolling, make this ViewGroup look like a + // ScrollView. This is kind of a hack until we have support for + // specifying auto-scroll behavior. + return android.widget.ScrollView.class.getName(); + } + + @Override + public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(info); + + if (isEnabled()) { + if (mCollapseOffset != 0) { + info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD); + info.addAction(AccessibilityAction.ACTION_EXPAND); + info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN); + info.setScrollable(true); + } + if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight) + && ((mCollapseOffset < mCollapsibleHeight) || isDismissable())) { + info.addAction(AccessibilityAction.ACTION_SCROLL_UP); + info.setScrollable(true); + } + if (mCollapseOffset < mCollapsibleHeight) { + info.addAction(AccessibilityAction.ACTION_COLLAPSE); + } + if (mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight && isDismissable()) { + info.addAction(AccessibilityAction.ACTION_DISMISS); + } + } + + // This view should never get accessibility focus, but it's interactive + // via nested scrolling, so we can't hide it completely. + info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS); + } + + @Override + public boolean performAccessibilityActionInternal(int action, Bundle arguments) { + if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) { + // This view should never get accessibility focus. + return false; + } + + if (super.performAccessibilityActionInternal(action, arguments)) { + return true; + } + + return performAccessibilityActionCommon(action); + } + + @Override + public void onDrawForeground(Canvas canvas) { + if (mScrollIndicatorDrawable != null) { + mScrollIndicatorDrawable.draw(canvas); + } + + super.onDrawForeground(canvas); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec); + int widthSize = sourceWidth; + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + // Single-use layout; just ignore the mode and use available space. + // Clamp to maxWidth. + if (mMaxWidth >= 0) { + widthSize = Math.min(widthSize, mMaxWidth + getPaddingLeft() + getPaddingRight()); + } + + final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); + final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); + + // Currently we allot more height than is really needed so that the entirety of the + // sheet may be pulled up. + // TODO: Restrict the height here to be the right value. + int heightUsed = 0; + + // Measure always-show children first. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.alwaysShow && child.getVisibility() != GONE) { + if (lp.maxHeight != -1) { + final int remainingHeight = heightSize - heightUsed; + measureChildWithMargins(child, widthSpec, 0, + MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST), + lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0); + } else { + measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed); + } + heightUsed += child.getMeasuredHeight(); + } + } + + mAlwaysShowHeight = heightUsed; + + // And now the rest. + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.alwaysShow && child.getVisibility() != GONE) { + if (lp.maxHeight != -1) { + final int remainingHeight = heightSize - heightUsed; + measureChildWithMargins(child, widthSpec, 0, + MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST), + lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0); + } else { + measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed); + } + heightUsed += child.getMeasuredHeight(); + } + } + + final int oldCollapsibleHeight = mCollapsibleHeight; + mCollapsibleHeight = Math.max(0, + heightUsed - mAlwaysShowHeight - getMaxCollapsedHeight()); + mUncollapsibleHeight = heightUsed - mCollapsibleHeight; + + updateCollapseOffset(oldCollapsibleHeight, !isDragging()); + + if (getShowAtTop()) { + mTopOffset = 0; + } else { + mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset; + } + + setMeasuredDimension(sourceWidth, heightSize); + } + + /** + * @return The space reserved by views with 'alwaysShow=true' + */ + public int getAlwaysShowHeight() { + return mAlwaysShowHeight; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int width = getWidth(); + + View indicatorHost = null; + + int ypos = mTopOffset; + final int leftEdge = getPaddingLeft(); + final int rightEdge = width - getPaddingRight(); + final int widthAvailable = rightEdge - leftEdge; + + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.hasNestedScrollIndicator) { + indicatorHost = child; + } + + if (child.getVisibility() == GONE) { + continue; + } + + int top = ypos + lp.topMargin; + if (lp.ignoreOffset) { + top -= mCollapseOffset; + } + final int bottom = top + child.getMeasuredHeight(); + + final int childWidth = child.getMeasuredWidth(); + final int left = leftEdge + (widthAvailable - childWidth) / 2; + final int right = left + childWidth; + + child.layout(left, top, right, bottom); + + ypos = bottom + lp.bottomMargin; + } + + if (mScrollIndicatorDrawable != null) { + if (indicatorHost != null) { + final int left = indicatorHost.getLeft(); + final int right = indicatorHost.getRight(); + final int bottom = indicatorHost.getTop(); + final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight(); + mScrollIndicatorDrawable.setBounds(left, top, right, bottom); + setWillNotDraw(!isCollapsed()); + } else { + mScrollIndicatorDrawable = null; + setWillNotDraw(true); + } + } + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + if (p instanceof LayoutParams) { + return new LayoutParams((LayoutParams) p); + } else if (p instanceof MarginLayoutParams) { + return new LayoutParams((MarginLayoutParams) p); + } + return new LayoutParams(p); + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + + @Override + protected Parcelable onSaveInstanceState() { + final SavedState ss = new SavedState(super.onSaveInstanceState()); + ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0; + ss.mCollapsibleHeightReserved = mCollapsibleHeightReserved; + return ss; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + final SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mOpenOnLayout = ss.open; + mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved; + } + + public static class LayoutParams extends MarginLayoutParams { + public boolean alwaysShow; + public boolean ignoreOffset; + public boolean hasNestedScrollIndicator; + public int maxHeight; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + final TypedArray a = c.obtainStyledAttributes(attrs, + R.styleable.ResolverDrawerLayout_LayoutParams); + alwaysShow = a.getBoolean( + R.styleable.ResolverDrawerLayout_LayoutParams_layout_alwaysShow, + false); + ignoreOffset = a.getBoolean( + R.styleable.ResolverDrawerLayout_LayoutParams_layout_ignoreOffset, + false); + hasNestedScrollIndicator = a.getBoolean( + R.styleable.ResolverDrawerLayout_LayoutParams_layout_hasNestedScrollIndicator, + false); + maxHeight = a.getDimensionPixelSize( + R.styleable.ResolverDrawerLayout_LayoutParams_layout_maxHeight, -1); + a.recycle(); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(LayoutParams source) { + super(source); + this.alwaysShow = source.alwaysShow; + this.ignoreOffset = source.ignoreOffset; + this.hasNestedScrollIndicator = source.hasNestedScrollIndicator; + this.maxHeight = source.maxHeight; + } + + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + } + + static class SavedState extends BaseSavedState { + boolean open; + private int mCollapsibleHeightReserved; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + open = in.readInt() != 0; + mCollapsibleHeightReserved = in.readInt(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(open ? 1 : 0); + out.writeInt(mCollapsibleHeightReserved); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + /** + * Listener for sheet dismissed events. + */ + public interface OnDismissedListener { + /** + * Callback when the sheet is dismissed by the user. + */ + void onDismissed(); + } + + /** + * Listener for sheet collapsed / expanded events. + */ + public interface OnCollapsedChangedListener { + /** + * Callback when the sheet is either fully expanded or collapsed. + * @param isCollapsed true when collapsed, false when expanded. + */ + void onCollapsedChanged(boolean isCollapsed); + } + + private class RunOnDismissedListener implements Runnable { + @Override + public void run() { + dispatchOnDismissed(); + } + } + + private MetricsLogger getMetricsLogger() { + if (mMetricsLogger == null) { + mMetricsLogger = new MetricsLogger(); + } + return mMetricsLogger; + } +} diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 7a590584..9ac815f6 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -86,6 +86,8 @@ import android.view.View; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import androidx.test.espresso.matcher.BoundedDiagnosingMatcher; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; @@ -96,8 +98,6 @@ import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.FrameworkStatsLog; -import com.android.internal.widget.GridLayoutManager; -import com.android.internal.widget.RecyclerView; import org.hamcrest.Description; import org.hamcrest.Matcher; |