From 5e4370b5de1347909d514e279e1aada582458820 Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Tue, 1 Aug 2023 14:44:48 -0700 Subject: Make preview scrollable under a feature flag. Base feature implementation controlled by a flag. A few issues are known and will be addressed separately. Known issues: * No (dis)appearance animation for the A-Z targets divider bar. Bug: 287102904 Test: Enabled the flag programmatically and test the new functionality. Manually test for possible regressions with the unset flag. Test: atest com.android.intentresolver.contentpreview Test: atest IntentResolverUnitTests:UnbundledChooserActivityTest Change-Id: I8273cf365a1e00b1acff4030086f1a044ad7531f --- .../android/intentresolver/ChooserActivity.java | 29 ++++--- .../ChooserMultiProfilePagerAdapter.java | 25 ++++-- .../intentresolver/grid/ChooserGridAdapter.java | 34 +++++++- .../widget/ChooserNestedScrollView.kt | 90 ++++++++++++++++++++ .../widget/ResolverDrawerLayout.java | 96 +++++++++++++++++++++- 5 files changed, 252 insertions(+), 22 deletions(-) create mode 100644 java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 26dbd224..f455be4c 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -491,7 +491,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements /* workProfileQuietModeChecker= */ () -> false, /* workProfileUserHandle= */ null, getAnnotatedUserHandles().cloneProfileUserHandle, - mMaxTargetsPerRow); + mMaxTargetsPerRow, + mFeatureFlags); } private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles( @@ -525,7 +526,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements selectedProfile, getAnnotatedUserHandles().workProfileUserHandle, getAnnotatedUserHandles().cloneProfileUserHandle, - mMaxTargetsPerRow); + mMaxTargetsPerRow, + mFeatureFlags); } private int findSelectedProfile() { @@ -667,7 +669,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements getResources(), getLayoutInflater(), parent, - /*headlineViewParent=*/null); + mFeatureFlags.scrollablePreview() + ? findViewById(R.id.chooser_headline_row_container) + : null); if (layout != null) { adjustPreviewWidth(getResources().getConfiguration().orientation, layout); @@ -788,7 +792,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements @Override public int getLayoutResource() { - return R.layout.chooser_grid; + return mFeatureFlags.scrollablePreview() + ? R.layout.chooser_grid_scrollable_preview + : R.layout.chooser_grid; } @Override // ResolverListCommunicator @@ -1208,7 +1214,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements }, chooserListAdapter, shouldShowContentPreview(), - mMaxTargetsPerRow); + mMaxTargetsPerRow, + mFeatureFlags); } @VisibleForTesting @@ -1639,11 +1646,13 @@ public class ChooserActivity extends Hilt_ChooserActivity implements } private boolean shouldShowStickyContentPreviewNoOrientationCheck() { - return shouldShowTabs() - && (mMultiProfilePagerAdapter.getListAdapterForUserHandle( - UserHandle.of(UserHandle.myUserId())).getCount() > 0 - || shouldShowContentPreviewWhenEmpty()) - && shouldShowContentPreview(); + if (!shouldShowContentPreview()) { + return false; + } + boolean isEmpty = mMultiProfilePagerAdapter.getListAdapterForUserHandle( + UserHandle.of(UserHandle.myUserId())).getCount() == 0; + return (mFeatureFlags.scrollablePreview() || shouldShowTabs()) + && (!isEmpty || shouldShowContentPreviewWhenEmpty()); } /** diff --git a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java index c159243e..ba35ae5d 100644 --- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java @@ -52,7 +52,8 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda Supplier workProfileQuietModeChecker, UserHandle workProfileUserHandle, UserHandle cloneProfileUserHandle, - int maxTargetsPerRow) { + int maxTargetsPerRow, + FeatureFlags featureFlags) { this( context, new ChooserProfileAdapterBinder(maxTargetsPerRow), @@ -62,7 +63,8 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda /* defaultProfile= */ 0, workProfileUserHandle, cloneProfileUserHandle, - new BottomPaddingOverrideSupplier(context)); + new BottomPaddingOverrideSupplier(context), + featureFlags); } ChooserMultiProfilePagerAdapter( @@ -74,7 +76,8 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda @Profile int defaultProfile, UserHandle workProfileUserHandle, UserHandle cloneProfileUserHandle, - int maxTargetsPerRow) { + int maxTargetsPerRow, + FeatureFlags featureFlags) { this( context, new ChooserProfileAdapterBinder(maxTargetsPerRow), @@ -84,7 +87,8 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda defaultProfile, workProfileUserHandle, cloneProfileUserHandle, - new BottomPaddingOverrideSupplier(context)); + new BottomPaddingOverrideSupplier(context), + featureFlags); } private ChooserMultiProfilePagerAdapter( @@ -96,7 +100,8 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda @Profile int defaultProfile, UserHandle workProfileUserHandle, UserHandle cloneProfileUserHandle, - BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) { + BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier, + FeatureFlags featureFlags) { super( context, gridAdapter -> gridAdapter.getListAdapter(), @@ -107,7 +112,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda defaultProfile, workProfileUserHandle, cloneProfileUserHandle, - () -> makeProfileView(context), + () -> makeProfileView(context, featureFlags), bottomPaddingOverrideSupplier); mAdapterBinder = adapterBinder; mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier; @@ -131,10 +136,12 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda } } - private static ViewGroup makeProfileView(Context context) { + private static ViewGroup makeProfileView( + Context context, FeatureFlags featureFlags) { LayoutInflater inflater = LayoutInflater.from(context); - ViewGroup rootView = (ViewGroup) inflater.inflate( - R.layout.chooser_list_per_profile, null, false); + ViewGroup rootView = featureFlags.scrollablePreview() + ? (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile_wrap, null, false) + : (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile, null, false); RecyclerView recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list); recyclerView.setAccessibilityDelegateCompat( new ChooserRecyclerViewAccessibilityDelegate(recyclerView)); diff --git a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java index 77ae20f5..091ad158 100644 --- a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java +++ b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java @@ -32,9 +32,12 @@ import android.view.animation.DecelerateInterpolator; import android.widget.Space; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.android.intentresolver.ChooserListAdapter; +import com.android.intentresolver.FeatureFlags; import com.android.intentresolver.R; import com.android.intentresolver.ResolverListAdapter.ViewHolder; import com.android.internal.annotations.VisibleForTesting; @@ -107,6 +110,9 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter= 0) { + if (mRecyclerView != null) { + for (int i = 0, size = mRecyclerView.getChildCount(); i < size; i++) { + View child = mRecyclerView.getChildAt(i); + if (mRecyclerView.getChildAdapterPosition(child) == azRowPos) { + child.setVisibility(isVisible ? View.VISIBLE : View.GONE); + } + } + return; + } notifyItemChanged(azRowPos); } } diff --git a/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt b/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt new file mode 100644 index 00000000..26464ca1 --- /dev/null +++ b/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt @@ -0,0 +1,90 @@ +package com.android.intentresolver.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import androidx.core.view.ScrollingView +import androidx.core.view.marginBottom +import androidx.core.view.marginLeft +import androidx.core.view.marginRight +import androidx.core.view.marginTop +import androidx.core.widget.NestedScrollView + +/** + * A narrowly tailored [NestedScrollView] to be used inside [ResolverDrawerLayout] and help to + * orchestrate content preview scrolling. It expects one [LinearLayout] child with + * [LinearLayout.VERTICAL] orientation. If the child has more than one child, the first its child + * will be made scrollable (it is expected to be a content preview view). + */ +class ChooserNestedScrollView : NestedScrollView { + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int + ) : super(context, attrs, defStyleAttr) + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val content = + getChildAt(0) as? LinearLayout ?: error("Exactly one child, LinerLayout, is expected") + require(content.orientation == LinearLayout.VERTICAL) { "VERTICAL orientation is expected" } + require(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) { + "Expected to have an exact width" + } + + val lp = content.layoutParams ?: error("LayoutParams is missing") + val contentWidthSpec = + getChildMeasureSpec( + widthMeasureSpec, + paddingLeft + content.marginLeft + content.marginRight + paddingRight, + lp.width + ) + val contentHeightSpec = + getChildMeasureSpec( + heightMeasureSpec, + paddingTop + content.marginTop + content.marginBottom + paddingBottom, + lp.height + ) + content.measure(contentWidthSpec, contentHeightSpec) + + if (content.childCount > 1) { + // We expect that the first child should be scrollable up + val child = content.getChildAt(0) + val height = + MeasureSpec.getSize(heightMeasureSpec) + + child.measuredHeight + + child.marginTop + + child.marginBottom + + content.measure( + contentWidthSpec, + MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec)) + ) + } + setMeasuredDimension( + MeasureSpec.getSize(widthMeasureSpec), + minOf( + MeasureSpec.getSize(heightMeasureSpec), + paddingTop + + content.marginTop + + content.measuredHeight + + content.marginBottom + + paddingBottom + ) + ) + } + + override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) { + // let the parent scroll + super.onNestedPreScroll(target, dx, dy, consumed, type) + // scroll ourselves, if recycler has not scrolled + val delta = dy - consumed[1] + if (delta > 0 && target is ScrollingView && !target.canScrollVertically(-1)) { + val preScrollY = scrollY + scrollBy(0, delta) + consumed[1] += scrollY - preScrollY + } + } +} diff --git a/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java index de76a1d2..b8fbedbf 100644 --- a/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java +++ b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java @@ -45,6 +45,8 @@ import android.view.animation.AnimationUtils; import android.widget.AbsListView; import android.widget.OverScroller; +import androidx.annotation.Nullable; +import androidx.core.view.ScrollingView; import androidx.recyclerview.widget.RecyclerView; import com.android.intentresolver.R; @@ -131,6 +133,9 @@ public class ResolverDrawerLayout extends ViewGroup { private AbsListView mNestedListChild; private RecyclerView mNestedRecyclerChild; + @Nullable + private final ScrollablePreviewFlingLogicDelegate mFlingLogicDelegate; + private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener = new ViewTreeObserver.OnTouchModeChangeListener() { @Override @@ -167,6 +172,12 @@ public class ResolverDrawerLayout extends ViewGroup { mIgnoreOffsetTopLimitViewId = a.getResourceId( R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit, ID_NULL); } + mFlingLogicDelegate = + a.getBoolean( + R.styleable.ResolverDrawerLayout_useScrollablePreviewNestedFlingLogic, + false) + ? new ScrollablePreviewFlingLogicDelegate() {} + : null; a.recycle(); mScrollIndicatorDrawable = mContext.getDrawable( @@ -832,6 +843,9 @@ public class ResolverDrawerLayout extends ViewGroup { @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { + if (mFlingLogicDelegate != null) { + return mFlingLogicDelegate.onNestedPreFling(this, target, velocityX, velocityY); + } if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) { smoothScrollTo(0, velocityY); return true; @@ -841,9 +855,12 @@ public class ResolverDrawerLayout extends ViewGroup { @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + if (mFlingLogicDelegate != null) { + return mFlingLogicDelegate.onNestedFling(this, target, velocityX, velocityY, consumed); + } // TODO: find a more suitable way to fix it. // RecyclerView started reporting `consumed` as true whenever a scrolling is enabled, - // previously the value was based whether the fling can be performed in given direction + // previously the value was based on whether the fling can be performed in given direction // i.e. whether it is at the top or at the bottom. isRecyclerViewAtTheTop method is a // workaround that restores the legacy functionality. boolean shouldConsume = (Math.abs(velocityY) > mMinFlingVelocity) @@ -885,6 +902,13 @@ public class ResolverDrawerLayout extends ViewGroup { && firstChild.getTop() >= recyclerView.getPaddingTop(); } + private static boolean isFlingTargetAtTop(View target) { + if (target instanceof ScrollingView) { + return !target.canScrollVertically(-1); + } + return false; + } + private boolean performAccessibilityActionCommon(int action) { switch (action) { case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: @@ -1299,4 +1323,74 @@ public class ResolverDrawerLayout extends ViewGroup { } return mMetricsLogger; } + + /** + * Controlled by + * {@link com.android.intentresolver.Flags#FLAG_SCROLLABLE_PREVIEW} + */ + private interface ScrollablePreviewFlingLogicDelegate { + default boolean onNestedPreFling( + ResolverDrawerLayout drawer, View target, float velocityX, float velocityY) { + boolean shouldScroll = !drawer.getShowAtTop() && velocityY > drawer.mMinFlingVelocity + && drawer.mCollapseOffset != 0; + if (shouldScroll) { + drawer.smoothScrollTo(0, velocityY); + return true; + } + boolean shouldDismiss = (Math.abs(velocityY) > drawer.mMinFlingVelocity) + && velocityY < 0 + && isFlingTargetAtTop(target); + if (shouldDismiss) { + if (drawer.getShowAtTop()) { + drawer.smoothScrollTo(drawer.mCollapsibleHeight, velocityY); + } else { + if (drawer.isDismissable() + && drawer.mCollapseOffset > drawer.mCollapsibleHeight) { + drawer.smoothScrollTo(drawer.mHeightUsed, velocityY); + drawer.mDismissOnScrollerFinished = true; + } else { + drawer.smoothScrollTo(drawer.mCollapsibleHeight, velocityY); + } + } + return true; + } + return false; + } + + default boolean onNestedFling( + ResolverDrawerLayout drawer, + View target, + float velocityX, + float velocityY, + boolean consumed) { + // TODO: find a more suitable way to fix it. + // RecyclerView started reporting `consumed` as true whenever a scrolling is enabled, + // previously the value was based on whether the fling can be performed in given + // direction i.e. whether it is at the top or at the bottom. isRecyclerViewAtTheTop + // method is a workaround that restores the legacy functionality. + boolean shouldConsume = (Math.abs(velocityY) > drawer.mMinFlingVelocity) && !consumed; + if (shouldConsume) { + if (drawer.getShowAtTop()) { + if (drawer.isDismissable() && velocityY > 0) { + drawer.abortAnimation(); + drawer.dismiss(); + } else { + drawer.smoothScrollTo( + velocityY < 0 ? drawer.mCollapsibleHeight : 0, velocityY); + } + } else { + if (drawer.isDismissable() + && velocityY < 0 + && drawer.mCollapseOffset > drawer.mCollapsibleHeight) { + drawer.smoothScrollTo(drawer.mHeightUsed, velocityY); + drawer.mDismissOnScrollerFinished = true; + } else { + drawer.smoothScrollTo( + velocityY > 0 ? 0 : drawer.mCollapsibleHeight, velocityY); + } + } + } + return shouldConsume; + } + } } -- cgit v1.2.3-59-g8ed1b