• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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