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(/* animate */ true, Optional.of(result), /* requestFocus */ true);
105           }
106         });
107   }
108 
109   /**
110    * Expand the search bar and populate it with text if any exists.
111    *
112    * @param requestFocus should be false if showing the dialpad
113    */
expand(boolean animate, Optional<String> text, boolean requestFocus)114   /* package-private */ void expand(boolean animate, Optional<String> text, boolean requestFocus) {
115     if (isExpanded) {
116       return;
117     }
118 
119     int duration = animate ? ANIMATION_DURATION : 0;
120     searchBoxExpanded.setVisibility(VISIBLE);
121     AnimUtils.crossFadeViews(searchBoxExpanded, searchBoxCollapsed, duration);
122     ValueAnimator animator = ValueAnimator.ofFloat(EXPAND_MARGIN_FRACTION_START, 0f);
123     animator.addUpdateListener(animation -> setMargins((Float) animation.getAnimatedValue()));
124     animator.setDuration(duration);
125     animator.addListener(
126         new AnimatorListenerAdapter() {
127           @Override
128           public void onAnimationStart(Animator animation) {
129             super.onAnimationStart(animation);
130             DialerUtils.showInputMethod(searchBox);
131             isExpanded = true;
132           }
133 
134           @Override
135           public void onAnimationEnd(Animator animation) {
136             super.onAnimationEnd(animation);
137             if (text.isPresent()) {
138               searchBox.setText(text.get());
139             }
140             // Don't request focus unless we're actually showing the search box, otherwise
141             // physical/bluetooth keyboards will type into this box when the dialpad is open.
142             if (requestFocus) {
143               searchBox.requestFocus();
144             }
145             setBackgroundResource(R.drawable.search_bar_background);
146           }
147         });
148     animator.start();
149   }
150 
151   /** Collapse the search bar and clear it's text. */
collapse(boolean animate)152   /* package-private */ void collapse(boolean animate) {
153     if (!isExpanded) {
154       return;
155     }
156 
157     int duration = animate ? ANIMATION_DURATION : 0;
158     AnimUtils.crossFadeViews(searchBoxCollapsed, searchBoxExpanded, duration);
159     ValueAnimator animator = ValueAnimator.ofFloat(0f, EXPAND_MARGIN_FRACTION_START);
160     animator.addUpdateListener(animation -> setMargins((Float) animation.getAnimatedValue()));
161     animator.setDuration(duration);
162 
163     animator.addListener(
164         new AnimatorListenerAdapter() {
165           @Override
166           public void onAnimationStart(Animator animation) {
167             super.onAnimationStart(animation);
168             DialerUtils.hideInputMethod(searchBox);
169             isExpanded = false;
170           }
171 
172           @Override
173           public void onAnimationEnd(Animator animation) {
174             super.onAnimationEnd(animation);
175             searchBox.setText("");
176             searchBoxExpanded.setVisibility(INVISIBLE);
177             setBackgroundResource(R.drawable.search_bar_background_rounded_corners);
178           }
179         });
180     animator.start();
181   }
182 
183   /**
184    * Assigns margins to the search box as a fraction of its maximum margin size
185    *
186    * @param fraction How large the margins should be as a fraction of their full size
187    */
setMargins(float fraction)188   private void setMargins(float fraction) {
189     int margin = (int) (this.margin * fraction);
190     MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
191     params.topMargin = margin;
192     params.bottomMargin = margin;
193     params.leftMargin = margin;
194     params.rightMargin = margin;
195     searchBoxExpanded.getLayoutParams().height =
196         (int) (animationEndHeight - (animationEndHeight - animationStartHeight) * fraction);
197   }
198 
setSearchBarListener(@onNull SearchBarListener listener)199   /* package-private */ void setSearchBarListener(@NonNull SearchBarListener listener) {
200     this.listener = Assert.isNotNull(listener);
201   }
202 
getQuery()203   public String getQuery() {
204     return searchBox.getText().toString();
205   }
206 
isExpanded()207   public boolean isExpanded() {
208     return isExpanded;
209   }
210 
setQueryWithoutUpdate(String query)211   public void setQueryWithoutUpdate(String query) {
212     skipLatestTextChange = true;
213     searchBox.setText(query);
214     searchBox.setSelection(searchBox.getText().length());
215   }
216 
hideKeyboard()217   public void hideKeyboard() {
218     UiUtil.hideKeyboardFrom(getContext(), searchBox);
219   }
220 
showKeyboard()221   public void showKeyboard() {
222     UiUtil.forceOpenKeyboardFrom(getContext(), searchBox);
223   }
224 
setHint(@tringRes int hint)225   public void setHint(@StringRes int hint) {
226     searchBox.setHint(hint);
227     searchBoxTextView.setText(hint);
228   }
229 
230   /** Handles logic for text changes in the search box. */
231   private class SearchBoxTextWatcher implements TextWatcher {
232 
233     @Override
beforeTextChanged(CharSequence s, int start, int count, int after)234     public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
235 
236     @Override
onTextChanged(CharSequence s, int start, int before, int count)237     public void onTextChanged(CharSequence s, int start, int before, int count) {}
238 
239     @Override
afterTextChanged(Editable s)240     public void afterTextChanged(Editable s) {
241       clearButton.setVisibility(TextUtils.isEmpty(s) ? GONE : VISIBLE);
242       if (skipLatestTextChange) {
243         skipLatestTextChange = false;
244         return;
245       }
246 
247       // afterTextChanged is called each time the device is rotated (or the activity is recreated).
248       // That means that this method could potentially be called before the listener is set and
249       // we should check if it's null. In the case that it is null, assert that the query is empty
250       // because the listener must be notified of non-empty queries.
251       if (listener != null) {
252         listener.onSearchQueryUpdated(s.toString());
253       } else {
254         Assert.checkArgument(TextUtils.isEmpty(s.toString()));
255       }
256     }
257   }
258 }
259