Skip to content

Commit be09936

Browse files
imhappileticiarossi
authored andcommitted
[Search] Add a placeholder textview to Searchbar that keeps track of searchview edit text in order to gracefully fade it out in searchview collapse animation
PiperOrigin-RevId: 752904135
1 parent 571a196 commit be09936

File tree

5 files changed

+144
-55
lines changed

5 files changed

+144
-55
lines changed

catalog/java/io/material/catalog/search/SearchDemoUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ private static List<SuggestionItem> getThisWeekSuggestions() {
184184

185185
private static void submitSearchQuery(SearchBar searchBar, SearchView searchView, String query) {
186186
searchBar.setText(query);
187-
searchBar.post(() -> searchView.hide());
187+
searchView.hide();
188188
}
189189

190190
private static class SuggestionItem {

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

Lines changed: 79 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import android.view.ViewParent;
4848
import android.view.accessibility.AccessibilityNodeInfo;
4949
import android.widget.EditText;
50+
import android.widget.FrameLayout;
5051
import android.widget.ImageButton;
5152
import android.widget.TextView;
5253
import androidx.annotation.ColorInt;
@@ -136,6 +137,8 @@ public class SearchBar extends Toolbar {
136137
private static final String NAMESPACE_APP = "http://schemas.android.com/apk/res-auto";
137138

138139
private final TextView textView;
140+
private final TextView placeholderTextView;
141+
private final FrameLayout textViewContainer;
139142
private final int backgroundColor;
140143

141144
private boolean liftOnScroll;
@@ -228,12 +231,18 @@ public SearchBar(@NonNull Context context, @Nullable AttributeSet attrs, int def
228231
layoutInflated = true;
229232

230233
textView = findViewById(R.id.open_search_bar_text_view);
234+
placeholderTextView = findViewById(R.id.open_search_bar_placeholder_text_view);
235+
textViewContainer = findViewById(R.id.open_search_bar_text_view_container);
231236

232237
setElevation(elevation);
233238
initTextView(textAppearanceResId, text, hint);
234239
initBackground(shapeAppearanceModel, backgroundColor, elevation, strokeWidth, strokeColor);
235240
}
236241

242+
void setPlaceholderText(String string) {
243+
placeholderTextView.setText(string);
244+
}
245+
237246
private void validateAttributes(@Nullable AttributeSet attributeSet) {
238247
if (attributeSet == null) {
239248
return;
@@ -273,6 +282,7 @@ private void initNavigationIcon() {
273282
private void initTextView(@StyleRes int textAppearanceResId, String text, String hint) {
274283
if (textAppearanceResId != -1) {
275284
TextViewCompat.setTextAppearance(textView, textAppearanceResId);
285+
TextViewCompat.setTextAppearance(placeholderTextView, textAppearanceResId);
276286
}
277287
setText(text);
278288
setHint(hint);
@@ -423,7 +433,7 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto
423433
super.onLayout(changed, left, top, right, bottom);
424434

425435
if (centerView != null) {
426-
layoutViewInCenter(centerView, /* absolutePlacement= */ true);
436+
layoutViewInCenter(centerView);
427437
}
428438
setHandwritingBoundsInsets();
429439
if (textView != null) {
@@ -432,7 +442,9 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto
432442
// to be pushed to the side. In this case, we want the textview to be still centered on top of
433443
// any center views.
434444
if (textCentered) {
435-
layoutViewInCenter(textView, /* absolutePlacement= */ false);
445+
// Make sure textview does not overlap with any toolbar views (nav icon, menu) or
446+
// padding/insets
447+
layoutTextViewCenterAvoidToolbarViewsAndPadding();
436448
}
437449
}
438450
}
@@ -581,52 +593,72 @@ private ImageButton findOrGetNavView() {
581593
return navIconButton;
582594
}
583595

596+
private void layoutTextViewCenterAvoidToolbarViewsAndPadding() {
597+
int textViewContainerLeft = getMeasuredWidth() / 2 - textViewContainer.getMeasuredWidth() / 2;
598+
int textViewContainerRight = textViewContainerLeft + textViewContainer.getMeasuredWidth();
599+
int textViewContainerTop = getMeasuredHeight() / 2 - textViewContainer.getMeasuredHeight() / 2;
600+
int textViewContainerBottom = textViewContainerTop + textViewContainer.getMeasuredHeight();
601+
boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
602+
View menuView = findOrGetMenuView();
603+
View navIconButton = findOrGetNavView();
604+
605+
int textViewLeft = textViewContainer.getMeasuredWidth() / 2 - textView.getMeasuredWidth() / 2;
606+
int textViewRight = textViewLeft + textView.getMeasuredWidth();
607+
608+
// left and right refer to the textview's left and right coordinates within the searchbar
609+
int left = textViewLeft + textViewContainerLeft;
610+
int right = textViewContainerLeft + textViewRight;
611+
612+
View leftView = isRtl ? menuView : navIconButton;
613+
View rightView = isRtl ? navIconButton : menuView;
614+
int leftShift = 0;
615+
int rightShift = 0;
616+
if (leftView != null) {
617+
leftShift = max(leftView.getRight() - left, 0);
618+
}
619+
left += leftShift;
620+
right += leftShift;
621+
if (rightView != null) {
622+
rightShift = max(right - rightView.getLeft(), 0);
623+
}
624+
left -= rightShift;
625+
right -= rightShift;
626+
// Make sure to not lay out the view inside the SearchBar padding. paddingLeftAdded and
627+
// paddingRightAdded will never be non-zero at the same time, as Toolbar.measure has already
628+
// measured the children accounting for padding.
629+
int paddingLeftShift = max(getPaddingLeft() - left, getContentInsetLeft() - left);
630+
int paddingRightShift =
631+
max(
632+
right - (getMeasuredWidth() - getPaddingRight()),
633+
right - (getMeasuredWidth() - getContentInsetRight()));
634+
paddingLeftShift = max(paddingLeftShift, 0);
635+
paddingRightShift = max(paddingRightShift, 0);
636+
637+
int totalShift = leftShift - rightShift + paddingLeftShift - paddingRightShift;
638+
// Center the textViewContainer and shift over textViewContainer by the amount that textView
639+
// needs to be shifted over; this shifts both the container and the textView, which is necessary so the textView doesn't get
640+
// laid outside of the container.
641+
textViewContainer.layout(
642+
textViewContainerLeft + totalShift,
643+
textViewContainerTop,
644+
textViewContainerRight + totalShift,
645+
textViewContainerBottom);
646+
}
647+
584648
/**
585649
* Lays out the given view in the center of the {@link SearchBar}.
586650
*
587651
* @param view The view to layout in the center.
588-
* @param absolutePlacement Whether the view will be placed absolutely in the center (eg. ignoring
589-
* other toolbar elements like the nav icon and menu, padding, content insets)
590652
*/
591-
private void layoutViewInCenter(View view, boolean absolutePlacement) {
653+
private void layoutViewInCenter(View view) {
592654
if (view == null) {
593655
return;
594656
}
595657

596658
int viewWidth = view.getMeasuredWidth();
597659
int left = getMeasuredWidth() / 2 - viewWidth / 2;
598660
int right = left + viewWidth;
599-
if (!absolutePlacement) {
600-
View menuView = findOrGetMenuView();
601-
if (menuView != null) {
602-
int diff =
603-
getLayoutDirection() == LAYOUT_DIRECTION_RTL
604-
? max(menuView.getRight() - left, 0)
605-
: max(right - menuView.getLeft(), 0);
606-
left -= diff;
607-
right -= diff;
608-
}
609-
View navIcon = findOrGetNavView();
610-
if (navIcon != null) {
611-
int diff =
612-
getLayoutDirection() == LAYOUT_DIRECTION_RTL
613-
? max(right - navIcon.getLeft(), 0)
614-
: max(navIcon.getRight() - left, 0);
615-
left += diff;
616-
right += diff;
617-
}
618661

619-
// Make sure to not lay out the view inside the SearchBar padding. paddingLeftAdded and
620-
// paddingRightAdded will never be non-zero at the same time, as Toolbar.measure has already
621-
// measured the children accounting for padding.
622-
int paddingStartAdded = max(getPaddingStart() - left, getContentInsetStart() - left);
623-
int paddingEndAdded =
624-
max(
625-
right - (getMeasuredWidth() - getPaddingEnd()),
626-
right - (getMeasuredWidth() - getContentInsetEnd()));
627-
left += max(paddingStartAdded, 0) - max(paddingEndAdded, 0);
628-
right += max(paddingStartAdded, 0) - max(paddingEndAdded, 0);
629-
}
630662
int viewHeight = view.getMeasuredHeight();
631663
int top = getMeasuredHeight() / 2 - viewHeight / 2;
632664
int bottom = top + viewHeight;
@@ -690,6 +722,10 @@ public void setCenterView(@Nullable View view) {
690722
}
691723
}
692724

725+
TextView getPlaceholderTextView() {
726+
return placeholderTextView;
727+
}
728+
693729
/** Returns the main {@link TextView} which can be used for hint and search text. */
694730
@NonNull
695731
public TextView getTextView() {
@@ -705,26 +741,31 @@ public CharSequence getText() {
705741
/** Sets the text of main {@link TextView}. */
706742
public void setText(@Nullable CharSequence text) {
707743
textView.setText(text);
744+
placeholderTextView.setText(text);
708745
}
709746

710747
/** Sets the text of main {@link TextView}. */
711748
public void setText(@StringRes int textResId) {
712749
textView.setText(textResId);
750+
placeholderTextView.setText(textResId);
713751
}
714752

715-
/** Whether or not to center the text. */
753+
/** Whether or not to center the text within the TextView. */
716754
public void setTextCentered(boolean textCentered) {
717755
this.textCentered = textCentered;
718756
if (textView == null) {
719757
return;
720758
}
721-
Toolbar.LayoutParams lp = (LayoutParams) textView.getLayoutParams();
759+
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) textView.getLayoutParams();
722760
if (textCentered) {
761+
lp.gravity = Gravity.CENTER_HORIZONTAL;
723762
textView.setGravity(Gravity.CENTER_HORIZONTAL);
724763
} else {
764+
lp.gravity = Gravity.NO_GRAVITY;
725765
textView.setGravity(Gravity.NO_GRAVITY);
726766
}
727767
textView.setLayoutParams(lp);
768+
placeholderTextView.setLayoutParams(lp);
728769
}
729770

730771
/** Whether or not the text is centered. */
@@ -735,6 +776,7 @@ public boolean getTextCentered() {
735776
/** Clears the text of main {@link TextView}. */
736777
public void clearText() {
737778
textView.setText("");
779+
placeholderTextView.setText("");
738780
}
739781

740782
/** Returns the hint of main {@link TextView}. */

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,9 @@ public void startBackProgress(@NonNull BackEventCompat backEvent) {
288288
if (isHiddenOrHiding() || searchBar == null) {
289289
return;
290290
}
291+
if (searchBar != null) {
292+
searchBar.setPlaceholderText(editText.getText().toString());
293+
}
291294
searchViewAnimationHelper.startBackProgress(backEvent);
292295
}
293296

@@ -893,7 +896,12 @@ public void hide() {
893896
|| currentTransitionState.equals(TransitionState.HIDING)) {
894897
return;
895898
}
896-
searchViewAnimationHelper.hide();
899+
if (searchBar != null && searchBar.isAttachedToWindow()) {
900+
searchBar.setPlaceholderText(editText.getText().toString());
901+
searchBar.post(searchViewAnimationHelper::hide);
902+
} else {
903+
searchViewAnimationHelper.hide();
904+
}
897905
}
898906

899907
/** Updates the visibility of the {@link SearchView} without an animation. */

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

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,8 @@ private AnimatorSet getExpandCollapseAnimatorSet(boolean show) {
291291
getDummyToolbarAnimator(show),
292292
getActionMenuViewsAlphaAnimator(show),
293293
getEditTextAnimator(show),
294-
getSearchPrefixAnimator(show));
294+
getSearchPrefixAnimator(show),
295+
getTextAnimator(show));
295296
animatorSet.addListener(
296297
new AnimatorListenerAdapter() {
297298
@Override
@@ -302,6 +303,13 @@ public void onAnimationStart(Animator animation) {
302303
@Override
303304
public void onAnimationEnd(Animator animation) {
304305
setContentViewsAlpha(show ? 1 : 0);
306+
// Reset edittext and searchbar textview alphas after the animations are finished since
307+
// the visibilities for searchview and searchbar have been set accordingly.
308+
editText.setAlpha(1);
309+
if (searchBar != null) {
310+
searchBar.getTextView().setAlpha(1);
311+
}
312+
305313
// After expanding or collapsing, we should reset the clip bounds so it can react to the
306314
// screen or layout changes. Otherwise it will result in wrong clipping on the layout.
307315
rootView.resetClipBoundsAndCornerRadii();
@@ -569,23 +577,40 @@ private Animator getEditTextAnimator(boolean show) {
569577
return getTranslationAnimatorForText(show, editText);
570578
}
571579

580+
private AnimatorSet getTextAnimator(boolean show) {
581+
AnimatorSet animatorSet = new AnimatorSet();
582+
addTextFadeAnimatorIfNeeded(animatorSet);
583+
animatorSet.setDuration(show ? SHOW_DURATION_MS : HIDE_DURATION_MS);
584+
animatorSet.setInterpolator(
585+
ReversableAnimatedValueInterpolator.of(show, AnimationUtils.LINEAR_INTERPOLATOR));
586+
return animatorSet;
587+
}
588+
589+
private void addTextFadeAnimatorIfNeeded(AnimatorSet animatorSet) {
590+
if (searchBar == null || TextUtils.equals(editText.getText(), searchBar.getText())) {
591+
return;
592+
}
593+
// If the searchbar text is not equal to the searchview edittext, we want to fade out the
594+
// edittext and fade in the searchbar text
595+
ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
596+
animator.addUpdateListener(
597+
animation -> {
598+
editText.setAlpha((Float) animation.getAnimatedValue());
599+
searchBar.getTextView().setAlpha(1 - (Float) animation.getAnimatedValue());
600+
});
601+
animatorSet.playTogether(animator);
602+
}
603+
572604
private Animator getTranslationAnimatorForText(boolean show, View v) {
573-
TextView textView = searchBar.getTextView();
574-
int additionalMovement = 0;
575-
// If the text is centered and the text's hint is not equal to the text (ie. if there's any
576-
// extra space in between the textview's start and the actual text bounds)
577-
if (!TextUtils.isEmpty(textView.getText())
578-
&& searchBar.getTextCentered()
579-
&& textView.getHint() != textView.getText()) {
580-
String text = textView.getText().toString();
581-
Rect bounds = new Rect();
582-
textView.getPaint().getTextBounds(text, 0, text.length(), bounds);
583-
additionalMovement = max(0, searchBar.getTextView().getMeasuredWidth() / 2 - bounds.width() / 2);
605+
TextView textView = searchBar.getPlaceholderTextView();
606+
// If the placeholder text is empty, we animate to the searchbar textview instead.
607+
// Or if we're showing the searchview, we always animate from the searchbar textview, not
608+
// from the placeholder text.
609+
if (TextUtils.isEmpty(textView.getText()) || show) {
610+
textView = searchBar.getTextView();
584611
}
585612
int startX =
586-
getViewLeftFromSearchViewParent(searchBar.getTextView())
587-
+ additionalMovement
588-
- (v.getLeft() + textContainer.getLeft());
613+
getViewLeftFromSearchViewParent(textView) - (v.getLeft() + textContainer.getLeft());
589614
return getTranslationAnimator(show, v, startX, getFromTranslationY());
590615
}
591616

lib/java/com/google/android/material/search/res/layout/mtrl_search_bar.xml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,24 @@
1616
-->
1717
<!-- This text view isn't exposed to accessibility because its attributes are mirrored on the
1818
parent container to more closely resemble the behavior of an EditText. -->
19-
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
19+
<FrameLayout
20+
xmlns:android="http://schemas.android.com/apk/res/android"
21+
android:id="@+id/open_search_bar_text_view_container"
22+
android:layout_width="wrap_content"
23+
android:layout_height="wrap_content">
24+
<TextView
2025
android:id="@+id/open_search_bar_text_view"
2126
android:layout_width="wrap_content"
2227
android:layout_height="wrap_content"
2328
android:ellipsize="end"
2429
android:importantForAccessibility="no"
2530
android:maxLines="1"/>
31+
<TextView
32+
android:id="@+id/open_search_bar_placeholder_text_view"
33+
android:layout_width="wrap_content"
34+
android:layout_height="wrap_content"
35+
android:ellipsize="end"
36+
android:visibility="invisible"
37+
android:importantForAccessibility="no"
38+
android:maxLines="1"/>
39+
</FrameLayout>

0 commit comments

Comments
 (0)