diff options
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; |