/* * 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 transitionListeners = new LinkedHashSet<>(); @Nullable private SearchBar searchBar; private int softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; private boolean animatedNavigationIcon; private boolean animatedMenuItems; private boolean autoShowKeyboard; private boolean useWindowInsetsController; private boolean statusBarSpacerEnabledOverride; @NonNull private TransitionState currentTransitionState = TransitionState.HIDDEN; private Map childImportantForAccessibilityMap; public SearchView(@NonNull Context context) { this(context, null); } public SearchView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, R.attr.materialSearchViewStyle); } public SearchView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(wrap(context, attrs, defStyleAttr, DEF_STYLE_RES), attrs, defStyleAttr); // Ensure we are using the correctly themed context rather than the context that was passed in. context = getContext(); TypedArray a = ThemeEnforcement.obtainStyledAttributes( context, attrs, R.styleable.SearchView, defStyleAttr, DEF_STYLE_RES); int headerLayoutResId = a.getResourceId(R.styleable.SearchView_headerLayout, -1); int textAppearanceResId = a.getResourceId(R.styleable.SearchView_android_textAppearance, -1); String text = a.getString(R.styleable.SearchView_android_text); String hint = a.getString(R.styleable.SearchView_android_hint); String searchPrefixText = a.getString(R.styleable.SearchView_searchPrefixText); boolean useDrawerArrowDrawable = a.getBoolean(R.styleable.SearchView_useDrawerArrowDrawable, false); animatedNavigationIcon = a.getBoolean(R.styleable.SearchView_animateNavigationIcon, true); animatedMenuItems = a.getBoolean(R.styleable.SearchView_animateMenuItems, true); boolean hideNavigationIcon = a.getBoolean(R.styleable.SearchView_hideNavigationIcon, false); autoShowKeyboard = a.getBoolean(R.styleable.SearchView_autoShowKeyboard, true); a.recycle(); LayoutInflater.from(context).inflate(R.layout.mtrl_search_view, this); layoutInflated = true; scrim = findViewById(R.id.search_view_scrim); rootView = findViewById(R.id.search_view_root); backgroundView = findViewById(R.id.search_view_background); statusBarSpacer = findViewById(R.id.search_view_status_bar_spacer); headerContainer = findViewById(R.id.search_view_header_container); toolbarContainer = findViewById(R.id.search_view_toolbar_container); toolbar = findViewById(R.id.search_view_toolbar); dummyToolbar = findViewById(R.id.search_view_dummy_toolbar); searchPrefix = findViewById(R.id.search_view_search_prefix); editText = findViewById(R.id.search_view_edit_text); clearButton = findViewById(R.id.search_view_clear_button); divider = findViewById(R.id.search_view_divider); contentContainer = findViewById(R.id.search_view_content_container); searchViewAnimationHelper = new SearchViewAnimationHelper(this); elevationOverlayProvider = new ElevationOverlayProvider(context); setUpRootView(); setUpBackgroundViewElevationOverlay(); setUpHeaderLayout(headerLayoutResId); setSearchPrefixText(searchPrefixText); setUpEditText(textAppearanceResId, text, hint); setUpBackButton(useDrawerArrowDrawable, hideNavigationIcon); setUpClearButton(); setUpContentOnTouchListener(); setUpInsetListeners(); } @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { if (layoutInflated) { contentContainer.addView(child, index, params); } else { super.addView(child, index, params); } } @Override protected void onFinishInflate() { super.onFinishInflate(); updateSoftInputMode(); } @RequiresApi(VERSION_CODES.LOLLIPOP) @Override public void setElevation(float elevation) { super.setElevation(elevation); setUpBackgroundViewElevationOverlay(elevation); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); MaterialShapeUtils.setParentAbsoluteElevation(this); } @Override @NonNull public CoordinatorLayout.Behavior getBehavior() { return new SearchView.Behavior(); } @Nullable private Window getActivityWindow() { Activity activity = ContextUtils.getActivity(getContext()); return activity == null ? null : activity.getWindow(); } @SuppressLint("ClickableViewAccessibility") // Will be handled by accessibility delegate. private void setUpRootView() { rootView.setOnTouchListener((v, event) -> true); } private void setUpBackgroundViewElevationOverlay() { setUpBackgroundViewElevationOverlay(getOverlayElevation()); } private void setUpBackgroundViewElevationOverlay(float elevation) { if (elevationOverlayProvider == null || backgroundView == null) { return; } int backgroundColor = elevationOverlayProvider.compositeOverlayWithThemeSurfaceColorIfNeeded(elevation); backgroundView.setBackgroundColor(backgroundColor); } private float getOverlayElevation() { if (searchBar != null) { return searchBar.getCompatElevation(); } else { return getResources().getDimension(R.dimen.m3_searchview_elevation); } } private void setUpHeaderLayout(int headerLayoutResId) { if (headerLayoutResId != -1) { View headerView = LayoutInflater.from(getContext()).inflate(headerLayoutResId, headerContainer, false); addHeaderView(headerView); } } private void setUpEditText(@StyleRes int textAppearanceResId, String text, String hint) { if (textAppearanceResId != -1) { TextViewCompat.setTextAppearance(editText, textAppearanceResId); } editText.setText(text); editText.setHint(hint); } private void setUpBackButton(boolean useDrawerArrowDrawable, boolean hideNavigationIcon) { if (hideNavigationIcon) { toolbar.setNavigationIcon(null); return; } toolbar.setNavigationOnClickListener(v -> hide()); if (useDrawerArrowDrawable) { DrawerArrowDrawable drawerArrowDrawable = new DrawerArrowDrawable(getContext()); drawerArrowDrawable.setColor(MaterialColors.getColor(this, R.attr.colorOnSurface)); toolbar.setNavigationIcon(drawerArrowDrawable); } } private void setUpClearButton() { clearButton.setOnClickListener( v -> { clearText(); requestFocusAndShowKeyboardIfNeeded(); }); editText.addTextChangedListener( new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { clearButton.setVisibility(s.length() > 0 ? VISIBLE : GONE); } @Override public void afterTextChanged(Editable s) {} }); } @SuppressLint("ClickableViewAccessibility") // Will be handled by accessibility delegate. private void setUpContentOnTouchListener() { contentContainer.setOnTouchListener( (v, event) -> { if (isAdjustNothingSoftInputMode()) { clearFocusAndHideKeyboard(); } return false; }); } private void setUpStatusBarSpacer(@Px int height) { if (statusBarSpacer.getLayoutParams().height != height) { statusBarSpacer.getLayoutParams().height = height; statusBarSpacer.requestLayout(); } } @Px private int getStatusBarHeight() { @SuppressLint({ "DiscouragedApi", "InternalInsetResource" }) // Used for initial value. A WindowInsetsListener will apply correct insets later. int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); if (resourceId > 0) { return getResources().getDimensionPixelSize(resourceId); } else { return 0; } } /** * Note: DrawerArrowDrawable supports RTL, so there is no need to update the navigation icon for * RTL if it is a DrawerArrowDrawable. */ private void updateNavigationIconIfNeeded() { if (toolbar == null) { return; } if (isNavigationIconDrawerArrowDrawable(toolbar)) { return; } int navigationIcon = R.drawable.ic_arrow_back_black_24; if (searchBar == null) { toolbar.setNavigationIcon(navigationIcon); } else { Drawable navigationIconDrawable = DrawableCompat.wrap( AppCompatResources.getDrawable(getContext(), navigationIcon).mutate()); if (toolbar.getNavigationIconTint() != null) { DrawableCompat.setTint(navigationIconDrawable, toolbar.getNavigationIconTint()); } toolbar.setNavigationIcon( new FadeThroughDrawable(searchBar.getNavigationIcon(), navigationIconDrawable)); updateNavigationIconProgressIfNeeded(); } } private boolean isNavigationIconDrawerArrowDrawable(@NonNull Toolbar toolbar) { return DrawableCompat.unwrap(toolbar.getNavigationIcon()) instanceof DrawerArrowDrawable; } /** * Listens to {@link WindowInsetsCompat} and adjusts layouts accordingly. * *

NOTE: window insets are only delivered if no other layout consumed them before. E.g.: * *

    *
  • by declaring {@code fitsSystemWindows=true} *
  • by consuming insets via specific consume-methods (e.g. {@link * WindowInsetsCompat#consumeSystemWindowInsets()} *
*/ 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()); } updateNavigationIconIfNeeded(); setUpBackgroundViewElevationOverlay(); } /** * Add a header view to this {@link SearchView}, which will be placed above the search text area. * *

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 listeners = new LinkedHashSet<>(transitionListeners); for (TransitionListener listener : listeners) { listener.onStateChanged(this, previousState, state); } } /** Returns whether the {@link SearchView}'s main content view is shown or showing. */ public boolean isShowing() { return currentTransitionState.equals(TransitionState.SHOWN) || currentTransitionState.equals(TransitionState.SHOWING); } /** * Shows the {@link SearchView} with an animation. * *

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 { public Behavior() {} public Behavior(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } @Override public boolean onDependentViewChanged( @NonNull CoordinatorLayout parent, @NonNull SearchView child, @NonNull View dependency) { if (!child.isSetupWithSearchBar() && dependency instanceof SearchBar) { child.setupWithSearchBar((SearchBar) dependency); } return false; } } /** Callback interface that provides important transition events for a {@link SearchView}. */ public interface TransitionListener { /** Called when the given {@link SearchView SearchView's} transition state has changed. */ void onStateChanged( @NonNull SearchView searchView, @NonNull TransitionState previousState, @NonNull TransitionState newState); } /** Enum that defines the possible transition states of an {@link SearchView}. */ public enum TransitionState { HIDING, HIDDEN, SHOWING, SHOWN, } @Override @NonNull protected Parcelable onSaveInstanceState() { SavedState savedState = new SavedState(super.onSaveInstanceState()); CharSequence text = getText(); savedState.text = text == null ? null : text.toString(); savedState.visibility = rootView.getVisibility(); return savedState; } @Override protected void onRestoreInstanceState(Parcelable state) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } SavedState savedState = (SavedState) state; super.onRestoreInstanceState(savedState.getSuperState()); setText(savedState.text); setVisible(savedState.visibility == VISIBLE); } static class SavedState extends AbsSavedState { String text; int visibility; public SavedState(Parcel source) { this(source, null); } public SavedState(Parcel source, @Nullable ClassLoader classLoader) { super(source, classLoader); text = source.readString(); visibility = source.readInt(); } public SavedState(Parcelable superState) { super(superState); } public static final Parcelable.Creator CREATOR = new ClassLoaderCreator() { @Override public SavedState createFromParcel(Parcel source, ClassLoader loader) { return new SavedState(source, loader); } @Override public SavedState createFromParcel(Parcel source) { return new SavedState(source); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeString(text); dest.writeInt(visibility); } } }