1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.dialer.main.impl.toolbar; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ValueAnimator; 22 import android.content.Context; 23 import android.support.annotation.NonNull; 24 import android.support.annotation.Nullable; 25 import android.support.annotation.StringRes; 26 import android.text.Editable; 27 import android.text.TextUtils; 28 import android.text.TextWatcher; 29 import android.util.AttributeSet; 30 import android.view.View; 31 import android.widget.EditText; 32 import android.widget.FrameLayout; 33 import android.widget.TextView; 34 import com.android.dialer.animation.AnimUtils; 35 import com.android.dialer.common.Assert; 36 import com.android.dialer.common.UiUtil; 37 import com.android.dialer.util.DialerUtils; 38 import com.google.common.base.Optional; 39 40 /** Search bar for {@link MainToolbar}. Mostly used to handle expand and collapse animation. */ 41 final class SearchBarView extends FrameLayout { 42 43 private static final int ANIMATION_DURATION = 200; 44 private static final float EXPAND_MARGIN_FRACTION_START = 0.8f; 45 46 private final float margin; 47 private final float animationEndHeight; 48 private final float animationStartHeight; 49 50 private SearchBarListener listener; 51 private EditText searchBox; 52 private TextView searchBoxTextView; 53 // This useful for when the query didn't actually change. We want to avoid making excessive calls 54 // where we can since IPCs can take a long time on slow networks. 55 private boolean skipLatestTextChange; 56 57 private boolean isExpanded; 58 private View searchBoxCollapsed; 59 private View searchBoxExpanded; 60 private View clearButton; 61 SearchBarView(@onNull Context context, @Nullable AttributeSet attrs)62 public SearchBarView(@NonNull Context context, @Nullable AttributeSet attrs) { 63 super(context, attrs); 64 margin = getContext().getResources().getDimension(R.dimen.search_bar_margin); 65 animationEndHeight = 66 getContext().getResources().getDimension(R.dimen.expanded_search_bar_height); 67 animationStartHeight = 68 getContext().getResources().getDimension(R.dimen.collapsed_search_bar_height); 69 } 70 71 @Override onFinishInflate()72 protected void onFinishInflate() { 73 super.onFinishInflate(); 74 clearButton = findViewById(R.id.search_clear_button); 75 searchBox = findViewById(R.id.search_view); 76 searchBoxTextView = findViewById(R.id.search_box_start_search); 77 searchBoxCollapsed = findViewById(R.id.search_box_collapsed); 78 searchBoxExpanded = findViewById(R.id.search_box_expanded); 79 80 setOnClickListener(v -> listener.onSearchBarClicked()); 81 findViewById(R.id.voice_search_button).setOnClickListener(v -> voiceSearchClicked()); 82 findViewById(R.id.search_back_button).setOnClickListener(v -> onSearchBackButtonClicked()); 83 clearButton.setOnClickListener(v -> onSearchClearButtonClicked()); 84 searchBox.addTextChangedListener(new SearchBoxTextWatcher()); 85 } 86 onSearchClearButtonClicked()87 private void onSearchClearButtonClicked() { 88 searchBox.setText(""); 89 } 90 onSearchBackButtonClicked()91 private void onSearchBackButtonClicked() { 92 if (!isExpanded) { 93 return; 94 } 95 96 listener.onSearchBackButtonClicked(); 97 collapse(true); 98 } 99 voiceSearchClicked()100 private void voiceSearchClicked() { 101 listener.onVoiceButtonClicked( 102 result -> { 103 if (!TextUtils.isEmpty(result)) { 104 expand(true, Optional.of(result)); 105 } 106 }); 107 } 108 109 /** Expand the search bar and populate it with text if any exists. */ expand(boolean animate, Optional<String> text)110 /* package-private */ void expand(boolean animate, Optional<String> text) { 111 if (isExpanded) { 112 return; 113 } 114 115 int duration = animate ? ANIMATION_DURATION : 0; 116 searchBoxExpanded.setVisibility(VISIBLE); 117 AnimUtils.crossFadeViews(searchBoxExpanded, searchBoxCollapsed, duration); 118 ValueAnimator animator = ValueAnimator.ofFloat(EXPAND_MARGIN_FRACTION_START, 0f); 119 animator.addUpdateListener(animation -> setMargins((Float) animation.getAnimatedValue())); 120 animator.setDuration(duration); 121 animator.addListener( 122 new AnimatorListenerAdapter() { 123 @Override 124 public void onAnimationStart(Animator animation) { 125 super.onAnimationStart(animation); 126 DialerUtils.showInputMethod(searchBox); 127 isExpanded = true; 128 } 129 130 @Override 131 public void onAnimationEnd(Animator animation) { 132 super.onAnimationEnd(animation); 133 if (text.isPresent()) { 134 searchBox.setText(text.get()); 135 } 136 searchBox.requestFocus(); 137 setBackgroundResource(R.drawable.search_bar_background); 138 } 139 }); 140 animator.start(); 141 } 142 143 /** Collapse the search bar and clear it's text. */ collapse(boolean animate)144 /* package-private */ void collapse(boolean animate) { 145 if (!isExpanded) { 146 return; 147 } 148 149 int duration = animate ? ANIMATION_DURATION : 0; 150 AnimUtils.crossFadeViews(searchBoxCollapsed, searchBoxExpanded, duration); 151 ValueAnimator animator = ValueAnimator.ofFloat(0f, EXPAND_MARGIN_FRACTION_START); 152 animator.addUpdateListener(animation -> setMargins((Float) animation.getAnimatedValue())); 153 animator.setDuration(duration); 154 155 animator.addListener( 156 new AnimatorListenerAdapter() { 157 @Override 158 public void onAnimationStart(Animator animation) { 159 super.onAnimationStart(animation); 160 DialerUtils.hideInputMethod(searchBox); 161 isExpanded = false; 162 } 163 164 @Override 165 public void onAnimationEnd(Animator animation) { 166 super.onAnimationEnd(animation); 167 searchBox.setText(""); 168 searchBoxExpanded.setVisibility(INVISIBLE); 169 setBackgroundResource(R.drawable.search_bar_background_rounded_corners); 170 } 171 }); 172 animator.start(); 173 } 174 175 /** 176 * Assigns margins to the search box as a fraction of its maximum margin size 177 * 178 * @param fraction How large the margins should be as a fraction of their full size 179 */ setMargins(float fraction)180 private void setMargins(float fraction) { 181 int margin = (int) (this.margin * fraction); 182 MarginLayoutParams params = (MarginLayoutParams) getLayoutParams(); 183 params.topMargin = margin; 184 params.bottomMargin = margin; 185 params.leftMargin = margin; 186 params.rightMargin = margin; 187 searchBoxExpanded.getLayoutParams().height = 188 (int) (animationEndHeight - (animationEndHeight - animationStartHeight) * fraction); 189 requestLayout(); 190 } 191 setSearchBarListener(@onNull SearchBarListener listener)192 /* package-private */ void setSearchBarListener(@NonNull SearchBarListener listener) { 193 this.listener = Assert.isNotNull(listener); 194 } 195 getQuery()196 public String getQuery() { 197 return searchBox.getText().toString(); 198 } 199 isExpanded()200 public boolean isExpanded() { 201 return isExpanded; 202 } 203 setQueryWithoutUpdate(String query)204 public void setQueryWithoutUpdate(String query) { 205 skipLatestTextChange = true; 206 searchBox.setText(query); 207 searchBox.setSelection(searchBox.getText().length()); 208 } 209 hideKeyboard()210 public void hideKeyboard() { 211 UiUtil.hideKeyboardFrom(getContext(), searchBox); 212 } 213 showKeyboard()214 public void showKeyboard() { 215 UiUtil.openKeyboardFrom(getContext(), searchBox); 216 } 217 setHint(@tringRes int hint)218 public void setHint(@StringRes int hint) { 219 searchBox.setHint(hint); 220 searchBoxTextView.setText(hint); 221 } 222 223 /** Handles logic for text changes in the search box. */ 224 private class SearchBoxTextWatcher implements TextWatcher { 225 226 @Override beforeTextChanged(CharSequence s, int start, int count, int after)227 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 228 229 @Override onTextChanged(CharSequence s, int start, int before, int count)230 public void onTextChanged(CharSequence s, int start, int before, int count) {} 231 232 @Override afterTextChanged(Editable s)233 public void afterTextChanged(Editable s) { 234 clearButton.setVisibility(TextUtils.isEmpty(s) ? GONE : VISIBLE); 235 if (skipLatestTextChange) { 236 skipLatestTextChange = false; 237 return; 238 } 239 240 // afterTextChanged is called each time the device is rotated (or the activity is recreated). 241 // That means that this method could potentially be called before the listener is set and 242 // we should check if it's null. In the case that it is null, assert that the query is empty 243 // because the listener must be notified of non-empty queries. 244 if (listener != null) { 245 listener.onSearchQueryUpdated(s.toString()); 246 } else { 247 Assert.checkArgument(TextUtils.isEmpty(s.toString())); 248 } 249 } 250 } 251 } 252