/* * Copyright 2022 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 * * https://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.google.android.material.search; import com.google.android.material.R; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Parcel; import android.os.Parcelable; import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.graphics.drawable.DrawerArrowDrawable; import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.TextView; import androidx.activity.BackEventCompat; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.MenuRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.Px; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.annotation.StringRes; import androidx.annotation.StyleRes; import androidx.annotation.VisibleForTesting; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.core.widget.TextViewCompat; import androidx.customview.view.AbsSavedState; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.color.MaterialColors; import com.google.android.material.elevation.ElevationOverlayProvider; import com.google.android.material.internal.ClippableRoundedCornerLayout; import com.google.android.material.internal.ContextUtils; import com.google.android.material.internal.FadeThroughDrawable; import com.google.android.material.internal.ThemeEnforcement; import com.google.android.material.internal.ToolbarUtils; import com.google.android.material.internal.TouchObserverFrameLayout; import com.google.android.material.internal.ViewUtils; import com.google.android.material.motion.MaterialBackHandler; import com.google.android.material.motion.MaterialBackOrchestrator; import com.google.android.material.motion.MaterialMainContainerBackHelper; import com.google.android.material.shape.MaterialShapeUtils; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; /** * Layout that provides a full screen search view and can be used with {@link SearchBar}. * *
The example below shows how to use the {@link SearchBar} and {@link SearchView} together: * *
* <androidx.coordinatorlayout.widget.CoordinatorLayout * android:layout_width="match_parent" * android:layout_height="match_parent"> * * <!-- NestedScrollingChild goes here (NestedScrollView, RecyclerView, etc.). --> * <androidx.core.widget.NestedScrollView * android:layout_width="match_parent" * android:layout_height="match_parent" * app:layout_behavior="@string/searchbar_scrolling_view_behavior"> * <!-- Screen content goes here. --> * </androidx.core.widget.NestedScrollView> * * <com.google.android.material.appbar.AppBarLayout * android:layout_width="match_parent" * android:layout_height="wrap_content"> * <com.google.android.material.search.SearchBar * android:id="@+id/search_bar" * android:layout_width="match_parent" * android:layout_height="wrap_content" * android:hint="@string/searchbar_hint" /> * </com.google.android.material.appbar.AppBarLayout> * * <com.google.android.material.search.SearchView * android:layout_width="match_parent" * android:layout_height="match_parent" * android:hint="@string/searchbar_hint" * app:layout_anchor="@id/search_bar"> * <!-- Search suggestions/results go here (ScrollView, RecyclerView, etc.). --> * </com.google.android.material.search.SearchView> * </androidx.coordinatorlayout.widget.CoordinatorLayout> ** *
For more information, see the component
* developer guidance and design
* guidelines.
*/
@SuppressWarnings("RestrictTo")
public class SearchView extends FrameLayout
implements CoordinatorLayout.AttachedBehavior, MaterialBackHandler {
private static final long TALKBACK_FOCUS_CHANGE_DELAY_MS = 100;
private static final int DEF_STYLE_RES = R.style.Widget_Material3_SearchView;
final View scrim;
final ClippableRoundedCornerLayout rootView;
final View backgroundView;
final View statusBarSpacer;
final FrameLayout headerContainer;
final FrameLayout toolbarContainer;
final MaterialToolbar toolbar;
final Toolbar dummyToolbar;
final TextView searchPrefix;
final EditText editText;
final ImageButton clearButton;
final View divider;
final TouchObserverFrameLayout contentContainer;
private final boolean layoutInflated;
private final SearchViewAnimationHelper searchViewAnimationHelper;
@NonNull
private final MaterialBackOrchestrator backOrchestrator = new MaterialBackOrchestrator(this);
private final boolean backHandlingEnabled;
private final ElevationOverlayProvider elevationOverlayProvider;
private final Set NOTE: window insets are only delivered if no other layout consumed them before. E.g.:
*
* Note: due to complications with the expand/collapse animation, a header view is intended to
* be used with a standalone {@link SearchView} which slides up/down instead of morphing from an
* {@link SearchBar}.
*/
public void addHeaderView(@NonNull View headerView) {
headerContainer.addView(headerView);
headerContainer.setVisibility(VISIBLE);
}
/** Remove a header view from the section above the search text area. */
public void removeHeaderView(@NonNull View headerView) {
headerContainer.removeView(headerView);
if (headerContainer.getChildCount() == 0) {
headerContainer.setVisibility(GONE);
}
}
/** Remove all header views from the section above the search text area. */
public void removeAllHeaderViews() {
headerContainer.removeAllViews();
headerContainer.setVisibility(GONE);
}
/**
* Sets whether the navigation icon should be animated from the {@link SearchBar} to {@link
* SearchView}.
*/
public void setAnimatedNavigationIcon(boolean animatedNavigationIcon) {
this.animatedNavigationIcon = animatedNavigationIcon;
}
/**
* Returns whether the navigation icon should be animated from the {@link SearchBar} to {@link
* SearchView}.
*/
public boolean isAnimatedNavigationIcon() {
return animatedNavigationIcon;
}
/**
* Sets whether the menu items should be animated from the {@link SearchBar} to {@link
* SearchView}.
*/
public void setMenuItemsAnimated(boolean menuItemsAnimated) {
this.animatedMenuItems = menuItemsAnimated;
}
/**
* Returns whether the menu items should be animated from the {@link SearchBar} to {@link
* SearchView}.
*/
public boolean isMenuItemsAnimated() {
return animatedMenuItems;
}
/** Sets whether the soft keyboard should be shown when the {@link SearchView} is shown. */
public void setAutoShowKeyboard(boolean autoShowKeyboard) {
this.autoShowKeyboard = autoShowKeyboard;
}
/** Returns whether the soft keyboard should be shown when the {@link SearchView} is shown. */
public boolean isAutoShowKeyboard() {
return autoShowKeyboard;
}
/**
* Sets whether the soft keyboard should be shown with {@code WindowInsetsController}.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void setUseWindowInsetsController(boolean useWindowInsetsController) {
this.useWindowInsetsController = useWindowInsetsController;
}
/**
* Returns whether the soft keyboard should be shown with {@code WindowInsetsController}.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public boolean isUseWindowInsetsController() {
return useWindowInsetsController;
}
/** Adds a listener to handle {@link SearchView} transitions such as showing and closing. */
public void addTransitionListener(@NonNull TransitionListener transitionListener) {
transitionListeners.add(transitionListener);
}
/** Removes a listener to handle {@link SearchView} transitions such as showing and closing. */
public void removeTransitionListener(@NonNull TransitionListener transitionListener) {
transitionListeners.remove(transitionListener);
}
/** Inflate a menu to provide additional options. */
public void inflateMenu(@MenuRes int menuResId) {
toolbar.inflateMenu(menuResId);
}
/** Set a listener to handle menu item clicks. */
public void setOnMenuItemClickListener(
@Nullable OnMenuItemClickListener onMenuItemClickListener) {
toolbar.setOnMenuItemClickListener(onMenuItemClickListener);
}
/** Returns the search prefix {@link TextView}, which appears before the main {@link EditText}. */
@NonNull
public TextView getSearchPrefix() {
return searchPrefix;
}
/** Sets the search prefix text. */
public void setSearchPrefixText(@Nullable CharSequence searchPrefixText) {
searchPrefix.setText(searchPrefixText);
searchPrefix.setVisibility(TextUtils.isEmpty(searchPrefixText) ? GONE : VISIBLE);
}
/** Returns the search prefix text. */
@Nullable
public CharSequence getSearchPrefixText() {
return searchPrefix.getText();
}
/** Returns the {@link Toolbar} used by the {@link SearchView}. */
@NonNull
public Toolbar getToolbar() {
return toolbar;
}
/** Returns the main {@link EditText} which can be used for hint and search text. */
@NonNull
public EditText getEditText() {
return editText;
}
/** Returns the text of main {@link EditText}, which usually represents the search text. */
@SuppressLint("KotlinPropertyAccess") // Editable extends CharSequence.
@NonNull // EditText never returns null after initialization.
public Editable getText() {
return editText.getText();
}
/** Sets the text of main {@link EditText}. */
@SuppressLint("KotlinPropertyAccess") // Editable extends CharSequence.
public void setText(@Nullable CharSequence text) {
editText.setText(text);
}
/** Sets the text of main {@link EditText}. */
public void setText(@StringRes int textResId) {
editText.setText(textResId);
}
/** Clears the text of main {@link EditText}. */
public void clearText() {
editText.setText("");
}
/** Returns the hint of main {@link EditText}. */
@Nullable
public CharSequence getHint() {
return editText.getHint();
}
/** Sets the hint of main {@link EditText}. */
public void setHint(@Nullable CharSequence hint) {
editText.setHint(hint);
}
/** Sets the hint of main {@link EditText}. */
public void setHint(@StringRes int hintResId) {
editText.setHint(hintResId);
}
/** Returns the current value of this {@link SearchView}'s soft input mode. */
@SuppressLint("KotlinPropertyAccess") // This is a not property.
public int getSoftInputMode() {
return softInputMode;
}
/**
* Sets the soft input mode for this {@link SearchView}. This is important because the {@link
* SearchView} will use this to determine whether the keyboard should be shown/hidden at the same
* time as the expand/collapse animation, or if the keyboard should be staggered with the
* animation to avoid glitchiness due to a resize of the screen. This will be set automatically by
* the {@link SearchView} during initial render but make sure to invoke this if you are changing
* the soft input mode at runtime.
*/
public void updateSoftInputMode() {
Window window = getActivityWindow();
if (window != null) {
this.softInputMode = window.getAttributes().softInputMode;
}
}
/**
* Enables/disables the status bar spacer, which can be used in cases where the status bar is
* translucent and the {@link SearchView} should not overlap the status bar area. This will be set
* automatically by the {@link SearchView} during initial render, but make sure to invoke this if
* you would like to override the default behavior.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void setStatusBarSpacerEnabled(boolean enabled) {
statusBarSpacerEnabledOverride = true;
setStatusBarSpacerEnabledInternal(enabled);
}
private void setStatusBarSpacerEnabledInternal(boolean enabled) {
statusBarSpacer.setVisibility(enabled ? VISIBLE : GONE);
}
/** Returns the current {@link TransitionState} for this {@link SearchView}. */
@NonNull
public TransitionState getCurrentTransitionState() {
return currentTransitionState;
}
void setTransitionState(@NonNull TransitionState state) {
setTransitionState(state, /* updateModalForAccessibility= */ true);
}
private void setTransitionState(
@NonNull TransitionState state, boolean updateModalForAccessibility) {
if (currentTransitionState.equals(state)) {
return;
}
if (updateModalForAccessibility) {
if (state == TransitionState.SHOWN) {
setModalForAccessibility(true);
} else if (state == TransitionState.HIDDEN) {
setModalForAccessibility(false);
}
}
TransitionState previousState = currentTransitionState;
currentTransitionState = state;
Set Note: the show animation will not be started if the {@link SearchView} is currently shown or
* showing.
*/
public void show() {
if (currentTransitionState.equals(TransitionState.SHOWN)
|| currentTransitionState.equals(TransitionState.SHOWING)) {
return;
}
searchViewAnimationHelper.show();
}
/**
* Hides the {@link SearchView} with an animation.
*
* Note: the hide animation will not be started if the {@link SearchView} is currently hidden
* or hiding.
*/
public void hide() {
if (currentTransitionState.equals(TransitionState.HIDDEN)
|| currentTransitionState.equals(TransitionState.HIDING)) {
return;
}
searchViewAnimationHelper.hide();
}
/** Updates the visibility of the {@link SearchView} without an animation. */
public void setVisible(boolean visible) {
boolean wasVisible = rootView.getVisibility() == VISIBLE;
rootView.setVisibility(visible ? VISIBLE : GONE);
updateNavigationIconProgressIfNeeded();
setTransitionState(
visible ? TransitionState.SHOWN : TransitionState.HIDDEN,
/* updateModalForAccessibility= */ wasVisible != visible);
}
private void updateNavigationIconProgressIfNeeded() {
ImageButton backButton = ToolbarUtils.getNavigationIconButton(toolbar);
if (backButton == null) {
return;
}
int progress = rootView.getVisibility() == VISIBLE ? 1 : 0;
Drawable drawable = DrawableCompat.unwrap(backButton.getDrawable());
if (drawable instanceof DrawerArrowDrawable) {
((DrawerArrowDrawable) drawable).setProgress(progress);
}
if (drawable instanceof FadeThroughDrawable) {
((FadeThroughDrawable) drawable).setProgress(progress);
}
}
/**
* Requests focus on the main {@link EditText} and shows the soft keyboard if automatic showing of
* the keyboard is enabled.
*/
void requestFocusAndShowKeyboardIfNeeded() {
if (autoShowKeyboard) {
requestFocusAndShowKeyboard();
}
}
/** Requests focus on the main {@link EditText} and shows the soft keyboard. */
public void requestFocusAndShowKeyboard() {
// Without a delay requesting focus on edit text fails when talkback is active.
editText.postDelayed(
() -> {
if (editText.requestFocus()) {
// Workaround for talkback issue when clear button is clicked
editText.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
}
ViewUtils.showKeyboard(editText, useWindowInsetsController);
},
TALKBACK_FOCUS_CHANGE_DELAY_MS);
}
/** Clears focus on the main {@link EditText} and hides the soft keyboard. */
public void clearFocusAndHideKeyboard() {
editText.post(
() -> {
editText.clearFocus();
if (searchBar != null) {
searchBar.requestFocus();
}
ViewUtils.hideKeyboard(editText, useWindowInsetsController);
});
}
boolean isAdjustNothingSoftInputMode() {
return softInputMode == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
}
/**
* Sets whether the {@link SearchView} is modal for accessibility, i.e., whether views that are
* not nested within the {@link SearchView} are important for accessibility.
*/
public void setModalForAccessibility(boolean isSearchViewModal) {
ViewGroup rootView = (ViewGroup) this.getRootView();
if (isSearchViewModal) {
childImportantForAccessibilityMap = new HashMap<>(rootView.getChildCount());
}
updateChildImportantForAccessibility(rootView, isSearchViewModal);
if (!isSearchViewModal) {
// When SearchView is not modal, reset the important for accessibility map.
childImportantForAccessibilityMap = null;
}
}
/**
* Sets the 'touchscreenBlocksFocus' attribute of the nested toolbar. The attribute defaults to
* 'true' for API level 26+. We need to set it to 'false' if keyboard navigation is needed for the
* search results.
*/
public void setToolbarTouchscreenBlocksFocus(boolean touchscreenBlocksFocus) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
toolbar.setTouchscreenBlocksFocus(touchscreenBlocksFocus);
}
}
@SuppressLint("InlinedApi") // View Compat will handle the differences.
private void updateChildImportantForAccessibility(ViewGroup parent, boolean isSearchViewModal) {
for (int i = 0; i < parent.getChildCount(); i++) {
final View child = parent.getChildAt(i);
if (child == this) {
continue;
}
if (child.findViewById(this.rootView.getId()) != null) {
// If this child node contains SearchView, look at this node's children instead.
updateChildImportantForAccessibility((ViewGroup) child, isSearchViewModal);
continue;
}
if (!isSearchViewModal) {
if (childImportantForAccessibilityMap != null
&& childImportantForAccessibilityMap.containsKey(child)) {
// Restores the original important for accessibility value of the child view.
child.setImportantForAccessibility(childImportantForAccessibilityMap.get(child));
}
} else {
// Saves the important for accessibility value of the child view.
childImportantForAccessibilityMap.put(child, child.getImportantForAccessibility());
child.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
}
}
}
/**
* Provides the resource identifier for the back arrow icon.
*
* @hide
*/
@DrawableRes
@RestrictTo(LIBRARY_GROUP)
protected int getDefaultNavigationIconResource() {
return R.drawable.ic_arrow_back_black_24;
}
/** Behavior that sets up an {@link SearchView} with an {@link SearchBar}. */
public static class Behavior extends CoordinatorLayout.Behavior
*
*/
private void setUpInsetListeners() {
setUpToolbarInsetListener();
setUpDividerInsetListener();
setUpStatusBarSpacerInsetListener();
}
private void setUpToolbarInsetListener() {
ViewUtils.doOnApplyWindowInsets(
toolbar,
(view, insets, initialPadding) -> {
boolean isRtl = ViewUtils.isLayoutRtl(toolbar);
int paddingLeft = isRtl ? initialPadding.end : initialPadding.start;
int paddingRight = isRtl ? initialPadding.start : initialPadding.end;
toolbar.setPadding(
paddingLeft + insets.getSystemWindowInsetLeft(), initialPadding.top,
paddingRight + insets.getSystemWindowInsetRight(), initialPadding.bottom);
return insets;
});
}
private void setUpStatusBarSpacerInsetListener() {
// Set an initial height based on the default system value to support pre-L behavior.
setUpStatusBarSpacer(getStatusBarHeight());
// Listen to system window insets on L+ and adjusts status bar height based on the top inset.
ViewCompat.setOnApplyWindowInsetsListener(
statusBarSpacer,
(v, insets) -> {
int systemWindowInsetTop = insets.getSystemWindowInsetTop();
setUpStatusBarSpacer(systemWindowInsetTop);
if (!statusBarSpacerEnabledOverride) {
setStatusBarSpacerEnabledInternal(systemWindowInsetTop > 0);
}
return insets;
});
}
private void setUpDividerInsetListener() {
MarginLayoutParams layoutParams = (MarginLayoutParams) divider.getLayoutParams();
int leftMargin = layoutParams.leftMargin;
int rightMargin = layoutParams.rightMargin;
ViewCompat.setOnApplyWindowInsetsListener(
divider,
(v, insets) -> {
layoutParams.leftMargin = leftMargin + insets.getSystemWindowInsetLeft();
layoutParams.rightMargin = rightMargin + insets.getSystemWindowInsetRight();
return insets;
});
}
/** Returns whether or not this {@link SearchView} is set up with an {@link SearchBar}. */
public boolean isSetupWithSearchBar() {
return this.searchBar != null;
}
/**
* Sets up this {@link SearchView} with an {@link SearchBar}, which will result in the {@link
* SearchView} being shown when the {@link SearchBar} is clicked. This behavior will be set up
* automatically if the {@link SearchBar} and {@link SearchView} are in a {@link
* CoordinatorLayout} and the {@link SearchView} is anchored to the {@link SearchBar}.
*/
public void setupWithSearchBar(@Nullable SearchBar searchBar) {
this.searchBar = searchBar;
searchViewAnimationHelper.setSearchBar(searchBar);
if (searchBar != null) {
searchBar.setOnClickListener(v -> show());
if (VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE) {
try {
searchBar.setHandwritingDelegatorCallback(this::show);
editText.setIsHandwritingDelegate(true);
} catch (LinkageError e) {
// Running on a device with an older build of Android U
// TODO(b/274154553): Remove try/catch block after Android U Beta 1 is released
}
}
}
updateNavigationIconIfNeeded();
setUpBackgroundViewElevationOverlay();
updateListeningForBackCallbacks(getCurrentTransitionState());
}
/**
* Add a header view to this {@link SearchView}, which will be placed above the search text area.
*
*