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