diff --git a/docs/components/TopAppBar.md b/docs/components/TopAppBar.md index bc6f3638d41..7a0a208c048 100644 --- a/docs/components/TopAppBar.md +++ b/docs/components/TopAppBar.md @@ -373,6 +373,7 @@ Element | Attribute **`CollapsingToolbarLayout` scrim animation duration** | `app:scrimAnimationDuration` | `setScrimAnimationDuration`
`getScrimAnimationDuration` | `600` **`CollapsingToolbarLayout` collapsing animation interpolator** | `app:titlePositionInterpolator` | `setTitlePositionInterpolator` | `@null` **`AppBarLayout` lift on scroll** | `app:liftOnScroll` | `setLiftOnScroll`
`isLiftOnScroll` | `true` +**`AppBarLayout` lift on scroll color** | `app:liftOnScrollColor` | N/A | `@null` (defaults to elevation overlay color) **`AppBarLayout` lift on scroll target view** | `app:liftOnScrollTargetViewId` | `setLiftOnScrollTargetViewId`
`getLiftOnScrollTargetViewId` | `@null` #### `AppBarLayout` styles diff --git a/lib/java/com/google/android/material/appbar/AppBarLayout.java b/lib/java/com/google/android/material/appbar/AppBarLayout.java index 9b251000a32..2c95e68badb 100644 --- a/lib/java/com/google/android/material/appbar/AppBarLayout.java +++ b/lib/java/com/google/android/material/appbar/AppBarLayout.java @@ -74,6 +74,7 @@ import com.google.android.material.animation.AnimationUtils; import com.google.android.material.appbar.AppBarLayout.BaseBehavior.SavedState; import com.google.android.material.internal.ThemeEnforcement; +import com.google.android.material.resources.MaterialResources; import com.google.android.material.shape.MaterialShapeDrawable; import com.google.android.material.shape.MaterialShapeUtils; import java.lang.annotation.Retention; @@ -202,13 +203,17 @@ public interface LiftOnScrollListener { private boolean liftOnScroll; @IdRes private int liftOnScrollTargetViewId; @Nullable private WeakReference liftOnScrollTargetView; - @Nullable private ValueAnimator elevationOverlayAnimator; + @Nullable private final ColorStateList liftOnScrollColor; + @Nullable private ValueAnimator liftOnScrollColorAnimator; + @Nullable private AnimatorUpdateListener liftOnScrollColorUpdateListener; private final List liftOnScrollListeners = new ArrayList<>(); private int[] tmpStatesArray; @Nullable private Drawable statusBarForeground; + private final float appBarElevation; + private Behavior behavior; public AppBarLayout(@NonNull Context context) { @@ -243,11 +248,21 @@ public AppBarLayout(@NonNull Context context, @Nullable AttributeSet attrs, int ViewCompat.setBackground(this, a.getDrawable(R.styleable.AppBarLayout_android_background)); + liftOnScrollColor = + MaterialResources.getColorStateList( + context, a, R.styleable.AppBarLayout_liftOnScrollColor); + if (getBackground() instanceof ColorDrawable) { ColorDrawable background = (ColorDrawable) getBackground(); MaterialShapeDrawable materialShapeDrawable = new MaterialShapeDrawable(); materialShapeDrawable.setFillColor(ColorStateList.valueOf(background.getColor())); - materialShapeDrawable.initializeElevationOverlay(context); + // If there is a lift on scroll color specified, we do not initialize the elevation overlay + // and set the alpha to zero manually. + if (liftOnScrollColor != null) { + initializeLiftOnScrollWithColor(materialShapeDrawable); + } else { + initializeLiftOnScrollWithElevation(context, materialShapeDrawable); + } ViewCompat.setBackground(this, materialShapeDrawable); } @@ -276,6 +291,9 @@ public AppBarLayout(@NonNull Context context, @Nullable AttributeSet attrs, int } } + // TODO(b/249786834): This should be a customizable attribute. + appBarElevation = getResources().getDimension(R.dimen.design_appbar_elevation); + liftOnScroll = a.getBoolean(R.styleable.AppBarLayout_liftOnScroll, false); liftOnScrollTargetViewId = a.getResourceId(R.styleable.AppBarLayout_liftOnScrollTargetViewId, View.NO_ID); @@ -293,6 +311,37 @@ public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) }); } + private void initializeLiftOnScrollWithColor(MaterialShapeDrawable background) { + background.setAlpha(lifted ? 255 : 0); + background.setFillColor(liftOnScrollColor); + liftOnScrollColorUpdateListener = valueAnimator -> { + float alpha = (float) valueAnimator.getAnimatedValue(); + background.setAlpha((int) alpha); + + for (LiftOnScrollListener liftOnScrollListener : liftOnScrollListeners) { + if (background.getFillColor() != null) { + liftOnScrollListener.onUpdate( + 0, background.getFillColor().withAlpha((int) alpha).getDefaultColor()); + } + } + }; + } + + private void initializeLiftOnScrollWithElevation( + Context context, MaterialShapeDrawable background) { + background.initializeElevationOverlay(context); + liftOnScrollColorUpdateListener = valueAnimator -> { + float elevation = (float) valueAnimator.getAnimatedValue(); + background.setElevation(elevation); + if (statusBarForeground instanceof MaterialShapeDrawable) { + ((MaterialShapeDrawable) statusBarForeground).setElevation(elevation); + } + for (LiftOnScrollListener liftOnScrollListener : liftOnScrollListeners) { + liftOnScrollListener.onUpdate(elevation, background.getResolvedTintColor()); + } + }; + } + /** * Add a listener that will be called when the offset of this {@link AppBarLayout} changes. * @@ -930,42 +979,33 @@ boolean setLiftedState(boolean lifted, boolean force) { this.lifted = lifted; refreshDrawableState(); if (liftOnScroll && getBackground() instanceof MaterialShapeDrawable) { - startLiftOnScrollElevationOverlayAnimation((MaterialShapeDrawable) getBackground(), lifted); + if (liftOnScrollColor != null) { + startLiftOnScrollColorAnimation( + lifted ? 0 : 255, lifted ? 255 : 0); + } else { + startLiftOnScrollColorAnimation( + lifted ? 0 : appBarElevation, lifted ? appBarElevation : 0); + } } return true; } return false; } - private void startLiftOnScrollElevationOverlayAnimation( - @NonNull final MaterialShapeDrawable background, boolean lifted) { - float appBarElevation = getResources().getDimension(R.dimen.design_appbar_elevation); - float fromElevation = lifted ? 0 : appBarElevation; - float toElevation = lifted ? appBarElevation : 0; - - if (elevationOverlayAnimator != null) { - elevationOverlayAnimator.cancel(); + private void startLiftOnScrollColorAnimation( + float fromValue, float toValue) { + if (liftOnScrollColorAnimator != null) { + liftOnScrollColorAnimator.cancel(); } - elevationOverlayAnimator = ValueAnimator.ofFloat(fromElevation, toElevation); - elevationOverlayAnimator.setDuration( + liftOnScrollColorAnimator = ValueAnimator.ofFloat(fromValue, toValue); + liftOnScrollColorAnimator.setDuration( getResources().getInteger(R.integer.app_bar_elevation_anim_duration)); - elevationOverlayAnimator.setInterpolator(AnimationUtils.LINEAR_INTERPOLATOR); - elevationOverlayAnimator.addUpdateListener( - new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(@NonNull ValueAnimator valueAnimator) { - float elevation = (float) valueAnimator.getAnimatedValue(); - background.setElevation(elevation); - if (statusBarForeground instanceof MaterialShapeDrawable) { - ((MaterialShapeDrawable) statusBarForeground).setElevation(elevation); - } - for (LiftOnScrollListener liftOnScrollListener : liftOnScrollListeners) { - liftOnScrollListener.onUpdate(elevation, background.getResolvedTintColor()); - } - } - }); - elevationOverlayAnimator.start(); + liftOnScrollColorAnimator.setInterpolator(AnimationUtils.LINEAR_INTERPOLATOR); + if (liftOnScrollColorUpdateListener != null) { + liftOnScrollColorAnimator.addUpdateListener(liftOnScrollColorUpdateListener); + } + liftOnScrollColorAnimator.start(); } /** diff --git a/lib/java/com/google/android/material/appbar/res-public/values/public.xml b/lib/java/com/google/android/material/appbar/res-public/values/public.xml index 3731bb2795d..1b357e18594 100644 --- a/lib/java/com/google/android/material/appbar/res-public/values/public.xml +++ b/lib/java/com/google/android/material/appbar/res-public/values/public.xml @@ -70,6 +70,7 @@ + diff --git a/lib/java/com/google/android/material/appbar/res/values/attrs.xml b/lib/java/com/google/android/material/appbar/res/values/attrs.xml index 5a81d74a6dd..ed295be289b 100644 --- a/lib/java/com/google/android/material/appbar/res/values/attrs.xml +++ b/lib/java/com/google/android/material/appbar/res/values/attrs.xml @@ -58,6 +58,9 @@ If this id is not set, the {@link AppBarLayout} will use the target view provided by nested scrolling to determine whether it should be lifted. --> + + diff --git a/testing/java/com/google/android/material/testapp/res/layout/design_appbar_toolbar_liftonscroll_color.xml b/testing/java/com/google/android/material/testapp/res/layout/design_appbar_toolbar_liftonscroll_color.xml new file mode 100644 index 00000000000..2b45f976e13 --- /dev/null +++ b/testing/java/com/google/android/material/testapp/res/layout/design_appbar_toolbar_liftonscroll_color.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/tests/javatests/com/google/android/material/appbar/AppBarWithToolbarTest.java b/tests/javatests/com/google/android/material/appbar/AppBarWithToolbarTest.java index 003a4c4c1d7..e95bb5edfb1 100644 --- a/tests/javatests/com/google/android/material/appbar/AppBarWithToolbarTest.java +++ b/tests/javatests/com/google/android/material/appbar/AppBarWithToolbarTest.java @@ -29,6 +29,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.MediumTest; import androidx.test.runner.AndroidJUnit4; +import com.google.android.material.shape.MaterialShapeDrawable; import com.google.android.material.testapp.R; import org.junit.Test; import org.junit.runner.RunWith; @@ -376,4 +377,35 @@ public void testUpdateAccessibilityActionsWithSetScrollFlags() throws Throwable assertAccessibilityHasScrollBackwardAction(false); assertAccessibilityScrollable(false); } + + /** Tests the lift on scroll color of the app bar layout. */ + @Test + public void testLiftOnScrollColor() throws Throwable { + configureContent( + R.layout.design_appbar_toolbar_liftonscroll_color, + R.string.design_appbar_toolbar_scroll_tabs_pin); + + final int[] appbarOnScreenXY = new int[2]; + mAppBar.getLocationOnScreen(appbarOnScreenXY); + + final int originalAppbarBottom = appbarOnScreenXY[1] + mAppBar.getHeight(); + final int centerX = appbarOnScreenXY[0] + mAppBar.getWidth() / 2; + + final int appbarHeight = mAppBar.getHeight(); + final int longSwipeAmount = 3 * appbarHeight / 2; + + assertEquals(0, ((MaterialShapeDrawable) mAppBar.getBackground()).getAlpha()); + + // Perform a swipe-up gesture across the horizontal center of the screen. + performVerticalSwipeUpGesture( + R.id.coordinator_layout, + centerX, + originalAppbarBottom + 3 * longSwipeAmount / 2, + longSwipeAmount); + + assertEquals(255, ((MaterialShapeDrawable) mAppBar.getBackground()).getAlpha()); + assertEquals( + mAppBar.getResources().getColor(R.color.material_blue_grey_950), + ((MaterialShapeDrawable) mAppBar.getBackground()).getFillColor().getDefaultColor()); + } }