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