diff options
| author | 2023-09-20 15:02:21 +0000 | |
|---|---|---|
| committer | 2023-09-20 15:02:21 +0000 | |
| commit | 07c906b2c45e406effb57cc70c51e4fa12dc4c59 (patch) | |
| tree | 3a9c196a46423d815a52cb19f0655cbb580252c5 /java/src | |
| parent | c7bcd3858d5aa3b7750890abfec73f40e175357b (diff) | |
| parent | 5e4370b5de1347909d514e279e1aada582458820 (diff) | |
Merge "Make preview scrollable under a feature flag." into main
Diffstat (limited to 'java/src')
5 files changed, 252 insertions, 22 deletions
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<Boolean> 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 fadea934..2ab38f30 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<RecyclerView. private final boolean mShouldShowContentPreview; private final int mChooserWidthPixels; private final int mChooserRowTextOptionTranslatePixelSize; + private final FeatureFlags mFeatureFlags; + @Nullable + private RecyclerView mRecyclerView; private int mChooserTargetWidth = 0; @@ -119,7 +125,8 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView. ChooserActivityDelegate chooserActivityDelegate, ChooserListAdapter wrappedAdapter, boolean shouldShowContentPreview, - int maxTargetsPerRow) { + int maxTargetsPerRow, + FeatureFlags featureFlags) { super(); mChooserActivityDelegate = chooserActivityDelegate; @@ -133,6 +140,7 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView. mChooserWidthPixels = context.getResources().getDimensionPixelSize(R.dimen.chooser_width); mChooserRowTextOptionTranslatePixelSize = context.getResources().getDimensionPixelSize( R.dimen.chooser_row_text_option_translate); + mFeatureFlags = featureFlags; wrappedAdapter.registerDataSetObserver(new DataSetObserver() { @Override @@ -149,6 +157,18 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView. }); } + @Override + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { + if (mFeatureFlags.scrollablePreview()) { + mRecyclerView = recyclerView; + } + } + + @Override + public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { + mRecyclerView = null; + } + public void setFooterHeight(int height) { mFooterHeight = height; } @@ -198,7 +218,8 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView. public int getSystemRowCount() { // For the tabbed case we show the sticky content preview above the tabs, // please refer to shouldShowStickyContentPreview - if (mChooserActivityDelegate.shouldShowTabs()) { + if (mChooserActivityDelegate.shouldShowTabs() + || mFeatureFlags.scrollablePreview()) { return 0; } @@ -318,6 +339,15 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView. mAzLabelVisibility = isVisible; int azRowPos = getAzLabelRowPosition(); if (azRowPos >= 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; + } + } } |