Skip to content

Commit 36b285c

Browse files
imhappidsn5ft
authored andcommitted
[SearchBar] Add a lift on scroll color to SearchBar when used with AppBarLayout
PiperOrigin-RevId: 740478154
1 parent 41f5079 commit 36b285c

File tree

5 files changed

+210
-4
lines changed

5 files changed

+210
-4
lines changed

docs/components/Search.md

+57-1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ Element | Attribute | Related method(s)
107107
**Search hint** | `android:hint` | `setHint`<br/>`getHint` | `null`
108108
**Search text centered** | `app:textCentered` | `setTextCentered`<br/>`getTextCentered` | `false`
109109
**Color** | `app:backgroundTint` | -- | `?attr/colorSurfaceContainerHigh`
110+
**Lift On Scroll** | `app:liftOnScroll` | -- | `false`
111+
**Lift On Scroll Color** | `app:liftOnScrollColor` | -- | `?attr/colorSurfaceContainerHighest`
110112
**Flag for default margins** | `app:defaultMarginsEnabled` | -- | `true`
111113
**Flag for navigation icon** | `app:hideNavigationIcon` | -- | `false`
112114

@@ -126,7 +128,8 @@ Search view toolbar height theme attribute:
126128

127129
### Scrolling Behavior
128130

129-
The `SearchBar` can either be used as a fixed or scroll-away search field.
131+
The `SearchBar` can either be used as a fixed, scroll-away, or lift on scroll
132+
search field.
130133

131134
#### Fixed Mode
132135

@@ -153,6 +156,20 @@ Additionally, if your app is going edge-to-edge, consider adding
153156
`app:statusBarForeground="?attr/colorSurface"` to your `AppBarLayout` in order
154157
to avoid overlap between the `SearchBar` and status bar content on scroll.
155158

159+
#### Lift On Scroll Mode
160+
161+
To set up the lift on scroll mode, use a top-level `CoordinatorLayout` and place
162+
the `SearchBar` within an `AppBarLayout`. Then, place the `AppBarLayout` below
163+
the scrolling view (usually a `RecyclerView` or `NestedScrollView`) in the
164+
`CoordinatorLayout`, and set
165+
`app:layout_behavior="@string/appbar_scrolling_view_behavior"` on the scrolling
166+
view. On the `SearchBar`, set `app:liftOnScroll=true` and set a
167+
`app:liftOnScrollColor` to change the color of the `SearchBar` as the
168+
`AppBarLayout` is lifting.
169+
170+
See the [Putting it all together](#putting-it-all-together) section below for an
171+
example of how to set up this behavior.
172+
156173
### Toolbar Transitions
157174

158175
The `SearchBar` component also provides transitions to and from a `Toolbar`,
@@ -397,6 +414,45 @@ as well as the expand and collapse animations. If you can't use a
397414
`CoordinatorLayout`, instead you can call the `SearchView#setUpWithSearchBar`
398415
method to achieve the same result.
399416

417+
Alternatively, an example of the lift on scroll mode is below:
418+
419+
```xml
420+
<androidx.coordinatorlayout.widget.CoordinatorLayout
421+
android:layout_width="match_parent"
422+
android:layout_height="match_parent">
423+
424+
<!-- NestedScrollingChild goes here (NestedScrollView, RecyclerView, etc.). -->
425+
<androidx.core.widget.NestedScrollView
426+
android:layout_width="match_parent"
427+
android:layout_height="match_parent"
428+
app:layout_behavior="@string/appbar_scrolling_view_behavior">
429+
<!-- Screen content goes here. -->
430+
</androidx.core.widget.NestedScrollView>
431+
432+
<com.google.android.material.appbar.AppBarLayout
433+
android:layout_width="match_parent"
434+
android:layout_height="wrap_content">
435+
<com.google.android.material.search.SearchBar
436+
android:id="@+id/search_bar"
437+
android:layout_width="match_parent"
438+
android:layout_height="wrap_content"
439+
android:hint="@string/searchbar_hint"
440+
app:liftOnScroll="true"
441+
app:liftOnScrollColor="?attr/colorSurfaceContainerHighest"/>
442+
</com.google.android.material.appbar.AppBarLayout>
443+
444+
<com.google.android.material.search.SearchView
445+
android:layout_width="match_parent"
446+
android:layout_height="match_parent"
447+
android:hint="@string/searchbar_hint"
448+
app:layout_anchor="@id/search_bar">
449+
450+
<!-- Search suggestions/results go here (ScrollView, RecyclerView, etc.). -->
451+
452+
</com.google.android.material.search.SearchView>
453+
</androidx.coordinatorlayout.widget.CoordinatorLayout>
454+
```
455+
400456
## Predictive Back
401457

402458
The `SearchView` component automatically supports

lib/java/com/google/android/material/appbar/AppBarLayout.java

+68-2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import androidx.annotation.ColorInt;
5353
import androidx.annotation.Dimension;
5454
import androidx.annotation.DrawableRes;
55+
import androidx.annotation.FloatRange;
5556
import androidx.annotation.IdRes;
5657
import androidx.annotation.IntDef;
5758
import androidx.annotation.NonNull;
@@ -77,10 +78,12 @@
7778
import com.google.android.material.resources.MaterialResources;
7879
import com.google.android.material.shape.MaterialShapeDrawable;
7980
import com.google.android.material.shape.MaterialShapeUtils;
81+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
8082
import java.lang.annotation.Retention;
8183
import java.lang.annotation.RetentionPolicy;
8284
import java.lang.ref.WeakReference;
8385
import java.util.ArrayList;
86+
import java.util.LinkedHashSet;
8487
import java.util.List;
8588

8689
/**
@@ -177,11 +180,30 @@ public interface OnOffsetChangedListener extends BaseOnOffsetChangedListener<App
177180
/**
178181
* Definition for a callback to be invoked when the lift on scroll elevation and background color
179182
* change.
183+
*
184+
* @deprecated Use {@link LiftOnScrollProgressListener} instead
180185
*/
186+
@Deprecated
181187
public interface LiftOnScrollListener {
182188
void onUpdate(@Dimension float elevation, @ColorInt int backgroundColor);
183189
}
184190

191+
/**
192+
* Definition for a callback to be invoked when the lift on scroll progress has changed.
193+
*/
194+
public abstract static class LiftOnScrollProgressListener {
195+
196+
/**
197+
* Update method called when the lift progress is updated.
198+
*
199+
* @param elevation the elevation of the AppBarLayout
200+
* @param backgroundColor the background color of the AppBarLayout
201+
* @param progress the progress of the lift animation; 0 is unlifted, 1 is lifted.
202+
*/
203+
public abstract void onUpdate(
204+
@Dimension float elevation, @ColorInt int backgroundColor, @FloatRange(from = 0.0f, to = 1.0f) float progress);
205+
}
206+
185207
private static final int DEF_STYLE_RES = R.style.Widget_Design_AppBarLayout;
186208
private static final int INVALID_SCROLL_RANGE = -1;
187209

@@ -209,6 +231,8 @@ public interface LiftOnScrollListener {
209231
@Nullable private ValueAnimator liftOnScrollColorAnimator;
210232
@Nullable private AnimatorUpdateListener liftOnScrollColorUpdateListener;
211233
private final List<LiftOnScrollListener> liftOnScrollListeners = new ArrayList<>();
234+
private final LinkedHashSet<LiftOnScrollProgressListener> liftProgressListeners =
235+
new LinkedHashSet<>();
212236

213237
private final long liftOnScrollColorDuration;
214238
private final TimeInterpolator liftOnScrollColorInterpolator;
@@ -349,6 +373,12 @@ private void initializeLiftOnScrollWithColor(
349373
}
350374
}
351375
}
376+
377+
if (!liftProgressListeners.isEmpty()) {
378+
for (LiftOnScrollProgressListener liftProgressListener : liftProgressListeners) {
379+
liftProgressListener.onUpdate(0, mixedColor, liftProgress);
380+
}
381+
}
352382
};
353383

354384
setBackground(background);
@@ -367,6 +397,10 @@ private void initializeLiftOnScrollWithElevation(
367397
for (LiftOnScrollListener liftOnScrollListener : liftOnScrollListeners) {
368398
liftOnScrollListener.onUpdate(elevation, background.getResolvedTintColor());
369399
}
400+
for (LiftOnScrollProgressListener liftProgressListener : liftProgressListeners) {
401+
liftProgressListener.onUpdate(
402+
elevation, background.getResolvedTintColor(), elevation / appBarElevation);
403+
}
370404
};
371405

372406
setBackground(background);
@@ -415,21 +449,53 @@ public void removeOnOffsetChangedListener(OnOffsetChangedListener listener) {
415449
/**
416450
* Add a {@link LiftOnScrollListener} that will be called when the lift on scroll elevation and
417451
* background color of this {@link AppBarLayout} change.
452+
*
453+
* @deprecated Use {@link #addLiftOnScrollProgressListener(LiftOnScrollProgressListener)} instead.
418454
*/
455+
@Deprecated
419456
public void addLiftOnScrollListener(@NonNull LiftOnScrollListener liftOnScrollListener) {
420457
liftOnScrollListeners.add(liftOnScrollListener);
421458
}
422459

423-
/** Remove a previously added {@link LiftOnScrollListener}. */
460+
/**
461+
* Remove a previously added {@link LiftOnScrollListener}.
462+
*
463+
* @deprecated Use {@link #removeLiftOnScrollProgressListener(LiftOnScrollProgressListener)} instead.
464+
*/
465+
@CanIgnoreReturnValue
466+
@Deprecated
424467
public boolean removeLiftOnScrollListener(@NonNull LiftOnScrollListener liftOnScrollListener) {
425468
return liftOnScrollListeners.remove(liftOnScrollListener);
426469
}
427470

428-
/** Remove all previously added {@link LiftOnScrollListener}s. */
471+
/**
472+
* Remove all previously added {@link LiftOnScrollListener}s.
473+
*
474+
* @deprecated Use {@link #clearLiftOnScrollProgressListener()} instead.
475+
*/
476+
@Deprecated
429477
public void clearLiftOnScrollListener() {
430478
liftOnScrollListeners.clear();
431479
}
432480

481+
/**
482+
* Add a {@link LiftOnScrollProgressListener} that will be called when the lift on scroll progress changes
483+
*/
484+
public void addLiftOnScrollProgressListener(@NonNull LiftOnScrollProgressListener liftProgressListener) {
485+
liftProgressListeners.add(liftProgressListener);
486+
}
487+
488+
/** Remove a previously added {@link LiftOnScrollProgressListener}. */
489+
@CanIgnoreReturnValue
490+
public boolean removeLiftOnScrollProgressListener(@NonNull LiftOnScrollProgressListener liftProgressListener) {
491+
return liftProgressListeners.remove(liftProgressListener);
492+
}
493+
494+
/** Remove all previously added {@link LiftOnScrollProgressListener}s. */
495+
public void clearLiftOnScrollProgressListener() {
496+
liftProgressListeners.clear();
497+
}
498+
433499
/**
434500
* Set the drawable to use for the status bar foreground drawable. Providing null will disable the
435501
* scrim functionality.

lib/java/com/google/android/material/search/SearchBar.java

+79-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import android.view.LayoutInflater;
4545
import android.view.View;
4646
import android.view.ViewGroup;
47+
import android.view.ViewParent;
4748
import android.view.accessibility.AccessibilityNodeInfo;
4849
import android.widget.EditText;
4950
import android.widget.ImageButton;
@@ -63,9 +64,11 @@
6364
import androidx.core.widget.TextViewCompat;
6465
import androidx.customview.view.AbsSavedState;
6566
import com.google.android.material.appbar.AppBarLayout;
67+
import com.google.android.material.appbar.AppBarLayout.LiftOnScrollProgressListener;
6668
import com.google.android.material.color.MaterialColors;
6769
import com.google.android.material.internal.ThemeEnforcement;
6870
import com.google.android.material.internal.ToolbarUtils;
71+
import com.google.android.material.resources.MaterialResources;
6972
import com.google.android.material.shape.MaterialShapeDrawable;
7073
import com.google.android.material.shape.MaterialShapeUtils;
7174
import com.google.android.material.shape.ShapeAppearanceModel;
@@ -132,6 +135,10 @@ public class SearchBar extends Toolbar {
132135
private static final String NAMESPACE_APP = "http://schemas.android.com/apk/res-auto";
133136

134137
private final TextView textView;
138+
private final int backgroundColor;
139+
140+
private boolean liftOnScroll;
141+
@Nullable private final ColorStateList liftOnScrollColor;
135142
private final boolean layoutInflated;
136143
private final boolean defaultMarginsEnabled;
137144
private final SearchBarAnimationHelper searchBarAnimationHelper;
@@ -148,6 +155,19 @@ public class SearchBar extends Toolbar {
148155
private MaterialShapeDrawable backgroundShape;
149156
private boolean textCentered;
150157

158+
private final LiftOnScrollProgressListener liftColorListener =
159+
new LiftOnScrollProgressListener() {
160+
161+
@Override
162+
public void onUpdate(float elevation, int appBarLayoutColor, float progress) {
163+
if (liftOnScrollColor != null) {
164+
int mixedColor =
165+
MaterialColors.layer(backgroundColor, liftOnScrollColor.getDefaultColor(), progress);
166+
backgroundShape.setFillColor(ColorStateList.valueOf(mixedColor));
167+
}
168+
}
169+
};
170+
151171
public SearchBar(@NonNull Context context) {
152172
this(context, null);
153173
}
@@ -175,7 +195,9 @@ public SearchBar(@NonNull Context context, @Nullable AttributeSet attrs, int def
175195

176196
ShapeAppearanceModel shapeAppearanceModel =
177197
ShapeAppearanceModel.builder(context, attrs, defStyleAttr, DEF_STYLE_RES).build();
178-
int backgroundColor = a.getColor(R.styleable.SearchBar_backgroundTint, 0);
198+
backgroundColor = a.getColor(R.styleable.SearchBar_backgroundTint, 0);
199+
liftOnScrollColor =
200+
MaterialResources.getColorStateList(context, a, R.styleable.SearchBar_liftOnScrollColor);
179201
float elevation = a.getDimension(R.styleable.SearchBar_elevation, 0);
180202
defaultMarginsEnabled = a.getBoolean(R.styleable.SearchBar_defaultMarginsEnabled, true);
181203
defaultScrollFlagsEnabled = a.getBoolean(R.styleable.SearchBar_defaultScrollFlagsEnabled, true);
@@ -192,6 +214,7 @@ public SearchBar(@NonNull Context context, @Nullable AttributeSet attrs, int def
192214
float strokeWidth = a.getDimension(R.styleable.SearchBar_strokeWidth, -1);
193215
int strokeColor = a.getColor(R.styleable.SearchBar_strokeColor, Color.TRANSPARENT);
194216
textCentered = a.getBoolean(R.styleable.SearchBar_textCentered, false);
217+
liftOnScroll = a.getBoolean(R.styleable.SearchBar_liftOnScroll, false);
195218

196219
a.recycle();
197220

@@ -225,6 +248,18 @@ private void validateAttributes(@Nullable AttributeSet attributeSet) {
225248
}
226249
}
227250

251+
@Nullable
252+
private AppBarLayout getAppBarLayoutParentIfExists() {
253+
ViewParent v = getParent();
254+
while (v != null) {
255+
if (v instanceof AppBarLayout) {
256+
return (AppBarLayout) v;
257+
}
258+
v = v.getParent();
259+
}
260+
return null;
261+
}
262+
228263
private void initNavigationIcon() {
229264
// If no navigation icon, set up the default one; otherwise, re-set it for tinting if needed.
230265
setNavigationIcon(getNavigationIcon() == null ? defaultNavigationIcon : getNavigationIcon());
@@ -417,13 +452,56 @@ private int getNewMargin(int currentMargin) {
417452
return additionalMarginStart;
418453
}
419454

455+
/**
456+
* Sets whether the {@link SearchBar} lifts when a parent {@link AppBarLayout} lifts on scroll.
457+
*/
458+
public void setLiftOnScroll(boolean liftOnScroll) {
459+
this.liftOnScroll = liftOnScroll;
460+
if (liftOnScroll) {
461+
addLiftOnScrollProgressListener();
462+
} else {
463+
removeLiftOnScrollProgressListener();
464+
}
465+
}
466+
467+
/**
468+
* Returns whether or not the {@link SearchBar} lifts when a parent {@link AppBarLayout} lifts
469+
* on scroll.
470+
*/
471+
public boolean isLiftOnScroll() {
472+
return liftOnScroll;
473+
}
474+
475+
private void addLiftOnScrollProgressListener() {
476+
AppBarLayout appBarLayout = getAppBarLayoutParentIfExists();
477+
if (appBarLayout != null && liftOnScrollColor != null) {
478+
appBarLayout.addLiftOnScrollProgressListener(liftColorListener);
479+
}
480+
}
481+
482+
private void removeLiftOnScrollProgressListener() {
483+
AppBarLayout appBarLayout = getAppBarLayoutParentIfExists();
484+
if (appBarLayout != null) {
485+
appBarLayout.removeLiftOnScrollProgressListener(liftColorListener);
486+
}
487+
}
488+
420489
@Override
421490
protected void onAttachedToWindow() {
422491
super.onAttachedToWindow();
423492

424493
MaterialShapeUtils.setParentAbsoluteElevation(this, backgroundShape);
425494
setDefaultMargins();
426495
setOrClearDefaultScrollFlags();
496+
if (liftOnScroll) {
497+
addLiftOnScrollProgressListener();
498+
}
499+
}
500+
501+
@Override
502+
protected void onDetachedFromWindow() {
503+
super.onDetachedFromWindow();
504+
removeLiftOnScrollProgressListener();
427505
}
428506

429507
/**

lib/java/com/google/android/material/search/res/values/attrs.xml

+4
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@
6767
<!-- Whether the text corresponding to the `android:text` attribute or `android:hint` attribute
6868
should be centered horizontally within the searchbar. Default is false.-->
6969
<attr name="textCentered" format="boolean"/>
70+
<!-- Whether or not the search bar is lifted when a parent AppBarLayout is lifted when scrolling. -->
71+
<attr name="liftOnScroll"/>
72+
<!-- The color of the search bar when lifted. If null, the search bar color will not change. -->
73+
<attr name="liftOnScrollColor"/>
7074
</declare-styleable>
7175

7276
<declare-styleable name="SearchView">

lib/java/com/google/android/material/search/res/values/styles.xml

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
<item name="shapeAppearance">?attr/shapeAppearanceMediumComponent</item>
3131
<item name="shapeAppearanceOverlay">@style/ShapeAppearanceOverlay.Material3.SearchBar</item>
3232
<item name="materialThemeOverlay">@style/ThemeOverlay.Material3.Search</item>
33+
<item name="liftOnScroll">false</item>
34+
<item name="liftOnScrollColor">?attr/colorSurfaceContainerHighest</item>
3335

3436
<!-- On newer API levels, hide shadows while keeping elevation. -->
3537
<item name="android:outlineAmbientShadowColor" tools:targetApi="p">@android:color/transparent</item>

0 commit comments

Comments
 (0)