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