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