1 /*
2  * Copyright (C) 2014 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.app.widget;
18 
19 import android.animation.ValueAnimator;
20 import android.animation.ValueAnimator.AnimatorUpdateListener;
21 import android.content.Context;
22 import android.text.Editable;
23 import android.text.TextUtils;
24 import android.text.TextWatcher;
25 import android.util.AttributeSet;
26 import android.view.KeyEvent;
27 import android.view.View;
28 import android.widget.EditText;
29 import android.widget.FrameLayout;
30 import com.android.dialer.animation.AnimUtils;
31 import com.android.dialer.app.R;
32 import com.android.dialer.util.DialerUtils;
33 
34 public class SearchEditTextLayout extends FrameLayout {
35 
36   private static final float EXPAND_MARGIN_FRACTION_START = 0.8f;
37   private static final int ANIMATION_DURATION = 200;
38   /* Subclass-visible for testing */
39   protected boolean isExpanded = false;
40   protected boolean isFadedOut = false;
41   private OnKeyListener preImeKeyListener;
42   private int topMargin;
43   private int bottomMargin;
44   private int leftMargin;
45   private int rightMargin;
46   private float collapsedElevation;
47   private View collapsed;
48   private View expanded;
49   private EditText searchView;
50   private View searchIcon;
51   private View collapsedSearchBox;
52   private View voiceSearchButtonView;
53   private View overflowButtonView;
54   private View clearButtonView;
55 
56   private ValueAnimator animator;
57 
58   private Callback callback;
59 
60   private boolean voiceSearchEnabled;
61 
SearchEditTextLayout(Context context, AttributeSet attrs)62   public SearchEditTextLayout(Context context, AttributeSet attrs) {
63     super(context, attrs);
64   }
65 
setPreImeKeyListener(OnKeyListener listener)66   public void setPreImeKeyListener(OnKeyListener listener) {
67     preImeKeyListener = listener;
68   }
69 
setCallback(Callback listener)70   public void setCallback(Callback listener) {
71     callback = listener;
72   }
73 
setVoiceSearchEnabled(boolean enabled)74   public void setVoiceSearchEnabled(boolean enabled) {
75     voiceSearchEnabled = enabled;
76     updateVisibility(isExpanded);
77   }
78 
79   @Override
onFinishInflate()80   protected void onFinishInflate() {
81     MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
82     topMargin = params.topMargin;
83     bottomMargin = params.bottomMargin;
84     leftMargin = params.leftMargin;
85     rightMargin = params.rightMargin;
86 
87     collapsedElevation = getElevation();
88 
89     collapsed = findViewById(R.id.search_box_collapsed);
90     expanded = findViewById(R.id.search_box_expanded);
91     searchView = (EditText) expanded.findViewById(R.id.search_view);
92 
93     searchIcon = findViewById(R.id.search_magnifying_glass);
94     collapsedSearchBox = findViewById(R.id.search_box_start_search);
95     voiceSearchButtonView = findViewById(R.id.voice_search_button);
96     overflowButtonView = findViewById(R.id.dialtacts_options_menu_button);
97     clearButtonView = findViewById(R.id.search_close_button);
98 
99     // Convert a long click into a click to expand the search box. Touch events are also
100     // forwarded to the searchView. This accelerates the long-press scenario for copy/paste.
101     collapsed.setOnLongClickListener(
102         new OnLongClickListener() {
103           @Override
104           public boolean onLongClick(View view) {
105             collapsed.performClick();
106             return false;
107           }
108         });
109     collapsed.setOnTouchListener(
110         (v, event) -> {
111           searchView.onTouchEvent(event);
112           return false;
113         });
114 
115     searchView.setOnFocusChangeListener(
116         new OnFocusChangeListener() {
117           @Override
118           public void onFocusChange(View v, boolean hasFocus) {
119             if (hasFocus) {
120               DialerUtils.showInputMethod(v);
121             } else {
122               DialerUtils.hideInputMethod(v);
123             }
124           }
125         });
126 
127     searchView.setOnClickListener(
128         new View.OnClickListener() {
129           @Override
130           public void onClick(View v) {
131             if (callback != null) {
132               callback.onSearchViewClicked();
133             }
134           }
135         });
136 
137     searchView.addTextChangedListener(
138         new TextWatcher() {
139           @Override
140           public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
141 
142           @Override
143           public void onTextChanged(CharSequence s, int start, int before, int count) {
144             clearButtonView.setVisibility(TextUtils.isEmpty(s) ? View.GONE : View.VISIBLE);
145           }
146 
147           @Override
148           public void afterTextChanged(Editable s) {}
149         });
150 
151     findViewById(R.id.search_close_button)
152         .setOnClickListener(
153             new OnClickListener() {
154               @Override
155               public void onClick(View v) {
156                 searchView.setText(null);
157               }
158             });
159 
160     findViewById(R.id.search_back_button)
161         .setOnClickListener(
162             new OnClickListener() {
163               @Override
164               public void onClick(View v) {
165                 if (callback != null) {
166                   callback.onBackButtonClicked();
167                 }
168               }
169             });
170 
171     super.onFinishInflate();
172   }
173 
174   @Override
dispatchKeyEventPreIme(KeyEvent event)175   public boolean dispatchKeyEventPreIme(KeyEvent event) {
176     if (preImeKeyListener != null) {
177       if (preImeKeyListener.onKey(this, event.getKeyCode(), event)) {
178         return true;
179       }
180     }
181     return super.dispatchKeyEventPreIme(event);
182   }
183 
fadeOut()184   public void fadeOut() {
185     fadeOut(null);
186   }
187 
fadeOut(AnimUtils.AnimationCallback callback)188   public void fadeOut(AnimUtils.AnimationCallback callback) {
189     AnimUtils.fadeOut(this, ANIMATION_DURATION, callback);
190     isFadedOut = true;
191   }
192 
fadeIn()193   public void fadeIn() {
194     AnimUtils.fadeIn(this, ANIMATION_DURATION);
195     isFadedOut = false;
196   }
197 
fadeIn(AnimUtils.AnimationCallback callback)198   public void fadeIn(AnimUtils.AnimationCallback callback) {
199     AnimUtils.fadeIn(this, ANIMATION_DURATION, AnimUtils.NO_DELAY, callback);
200     isFadedOut = false;
201   }
202 
setVisible(boolean visible)203   public void setVisible(boolean visible) {
204     if (visible) {
205       setAlpha(1);
206       setVisibility(View.VISIBLE);
207       isFadedOut = false;
208     } else {
209       setAlpha(0);
210       setVisibility(View.GONE);
211       isFadedOut = true;
212     }
213   }
214 
expand(boolean animate, boolean requestFocus)215   public void expand(boolean animate, boolean requestFocus) {
216     updateVisibility(true /* isExpand */);
217 
218     if (animate) {
219       AnimUtils.crossFadeViews(expanded, collapsed, ANIMATION_DURATION);
220       animator = ValueAnimator.ofFloat(EXPAND_MARGIN_FRACTION_START, 0f);
221       setMargins(EXPAND_MARGIN_FRACTION_START);
222       prepareAnimator();
223     } else {
224       expanded.setVisibility(View.VISIBLE);
225       expanded.setAlpha(1);
226       setMargins(0f);
227       collapsed.setVisibility(View.GONE);
228     }
229 
230     // Set 9-patch background. This owns the padding, so we need to restore the original values.
231     int paddingTop = this.getPaddingTop();
232     int paddingStart = this.getPaddingStart();
233     int paddingBottom = this.getPaddingBottom();
234     int paddingEnd = this.getPaddingEnd();
235     setBackgroundResource(R.drawable.search_shadow);
236     setElevation(0);
237     setPaddingRelative(paddingStart, paddingTop, paddingEnd, paddingBottom);
238 
239     if (requestFocus) {
240       searchView.requestFocus();
241     }
242     isExpanded = true;
243   }
244 
collapse(boolean animate)245   public void collapse(boolean animate) {
246     updateVisibility(false /* isExpand */);
247 
248     if (animate) {
249       AnimUtils.crossFadeViews(collapsed, expanded, ANIMATION_DURATION);
250       animator = ValueAnimator.ofFloat(0f, 1f);
251       prepareAnimator();
252     } else {
253       collapsed.setVisibility(View.VISIBLE);
254       collapsed.setAlpha(1);
255       setMargins(1f);
256       expanded.setVisibility(View.GONE);
257     }
258 
259     isExpanded = false;
260     setElevation(collapsedElevation);
261     setBackgroundResource(R.drawable.rounded_corner);
262   }
263 
264   /**
265    * Updates the visibility of views depending on whether we will show the expanded or collapsed
266    * search view. This helps prevent some jank with the crossfading if we are animating.
267    *
268    * @param isExpand Whether we are about to show the expanded search box.
269    */
updateVisibility(boolean isExpand)270   private void updateVisibility(boolean isExpand) {
271     int collapsedViewVisibility = isExpand ? View.GONE : View.VISIBLE;
272     int expandedViewVisibility = isExpand ? View.VISIBLE : View.GONE;
273 
274     searchIcon.setVisibility(collapsedViewVisibility);
275     collapsedSearchBox.setVisibility(collapsedViewVisibility);
276     if (voiceSearchEnabled) {
277       voiceSearchButtonView.setVisibility(collapsedViewVisibility);
278     } else {
279       voiceSearchButtonView.setVisibility(View.GONE);
280     }
281     overflowButtonView.setVisibility(collapsedViewVisibility);
282     // TODO: Prevents keyboard from jumping up in landscape mode after exiting the
283     // SearchFragment when the query string is empty. More elegant fix?
284     // mExpandedSearchBox.setVisibility(expandedViewVisibility);
285     if (TextUtils.isEmpty(searchView.getText())) {
286       clearButtonView.setVisibility(View.GONE);
287     } else {
288       clearButtonView.setVisibility(expandedViewVisibility);
289     }
290   }
291 
prepareAnimator()292   private void prepareAnimator() {
293     if (animator != null) {
294       animator.cancel();
295     }
296 
297     animator.addUpdateListener(
298         new AnimatorUpdateListener() {
299           @Override
300           public void onAnimationUpdate(ValueAnimator animation) {
301             final Float fraction = (Float) animation.getAnimatedValue();
302             setMargins(fraction);
303           }
304         });
305 
306     animator.setDuration(ANIMATION_DURATION);
307     animator.start();
308   }
309 
isExpanded()310   public boolean isExpanded() {
311     return isExpanded;
312   }
313 
isFadedOut()314   public boolean isFadedOut() {
315     return isFadedOut;
316   }
317 
318   /**
319    * Assigns margins to the search box as a fraction of its maximum margin size
320    *
321    * @param fraction How large the margins should be as a fraction of their full size
322    */
setMargins(float fraction)323   private void setMargins(float fraction) {
324     MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
325     params.topMargin = (int) (topMargin * fraction);
326     params.bottomMargin = (int) (bottomMargin * fraction);
327     params.leftMargin = (int) (leftMargin * fraction);
328     params.rightMargin = (int) (rightMargin * fraction);
329     requestLayout();
330   }
331 
332   /** Listener for the back button next to the search view being pressed */
333   public interface Callback {
334 
onBackButtonClicked()335     void onBackButtonClicked();
336 
onSearchViewClicked()337     void onSearchViewClicked();
338   }
339 }
340