/* * 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_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.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.TextView; 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.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.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> **/ @SuppressWarnings("RestrictTo") public class SearchView extends FrameLayout implements CoordinatorLayout.AttachedBehavior { 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; 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.
@Nullable
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) {
if (currentTransitionState.equals(state)) {
return;
}
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();
setModalForAccessibility(true);
}
/**
* 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();
setModalForAccessibility(false);
}
/** 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();
if (wasVisible != visible) {
setModalForAccessibility(visible);
}
setTransitionState(visible ? TransitionState.SHOWN : TransitionState.HIDDEN);
}
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() {
editText.post(
() -> {
editText.requestFocus();
ViewUtils.showKeyboard(editText, useWindowInsetsController);
});
}
/** Clears focus on the main {@link EditText} and hides the soft keyboard. */
public void clearFocusAndHideKeyboard() {
editText.clearFocus();
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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && 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.
ViewCompat.setImportantForAccessibility(
child, childImportantForAccessibilityMap.get(child));
}
} else {
// Saves the important for accessibility value of the child view.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
childImportantForAccessibilityMap.put(child, child.getImportantForAccessibility());
}
ViewCompat.setImportantForAccessibility(
child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
}
}
}
/** Behavior that sets up an {@link SearchView} with an {@link SearchBar}. */
public static class Behavior extends CoordinatorLayout.Behavior