1 /* 2 * Copyright (C) 2018 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.main.impl.toolbar; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ValueAnimator; 22 import android.content.Context; 23 import android.support.annotation.NonNull; 24 import android.support.annotation.Nullable; 25 import android.support.annotation.StringRes; 26 import android.text.Editable; 27 import android.text.TextUtils; 28 import android.text.TextWatcher; 29 import android.util.AttributeSet; 30 import android.view.View; 31 import android.widget.EditText; 32 import android.widget.FrameLayout; 33 import android.widget.TextView; 34 import com.android.dialer.animation.AnimUtils; 35 import com.android.dialer.common.Assert; 36 import com.android.dialer.common.UiUtil; 37 import com.android.dialer.util.DialerUtils; 38 import com.google.common.base.Optional; 39 40 /** Search bar for {@link MainToolbar}. Mostly used to handle expand and collapse animation. */ 41 final class SearchBarView extends FrameLayout { 42 43 private static final int ANIMATION_DURATION = 200; 44 private static final float EXPAND_MARGIN_FRACTION_START = 0.8f; 45 46 private final float margin; 47 private final float animationEndHeight; 48 private final float animationStartHeight; 49 50 private SearchBarListener listener; 51 private EditText searchBox; 52 private TextView searchBoxTextView; 53 // This useful for when the query didn't actually change. We want to avoid making excessive calls 54 // where we can since IPCs can take a long time on slow networks. 55 private boolean skipLatestTextChange; 56 57 private boolean isExpanded; 58 private View searchBoxCollapsed; 59 private View searchBoxExpanded; 60 private View clearButton; 61 SearchBarView(@onNull Context context, @Nullable AttributeSet attrs)62 public SearchBarView(@NonNull Context context, @Nullable AttributeSet attrs) { 63 super(context, attrs); 64 margin = getContext().getResources().getDimension(R.dimen.search_bar_margin); 65 animationEndHeight = 66 getContext().getResources().getDimension(R.dimen.expanded_search_bar_height); 67 animationStartHeight = 68 getContext().getResources().getDimension(R.dimen.collapsed_search_bar_height); 69 } 70 71 @Override onFinishInflate()72 protected void onFinishInflate() { 73 super.onFinishInflate(); 74 clearButton = findViewById(R.id.search_clear_button); 75 searchBox = findViewById(R.id.search_view); 76 searchBoxTextView = findViewById(R.id.search_box_start_search); 77 searchBoxCollapsed = findViewById(R.id.search_box_collapsed); 78 searchBoxExpanded = findViewById(R.id.search_box_expanded); 79 80 setOnClickListener(v -> listener.onSearchBarClicked()); 81 findViewById(R.id.voice_search_button).setOnClickListener(v -> voiceSearchClicked()); 82 findViewById(R.id.search_back_button).setOnClickListener(v -> onSearchBackButtonClicked()); 83 clearButton.setOnClickListener(v -> onSearchClearButtonClicked()); 84 searchBox.addTextChangedListener(new SearchBoxTextWatcher()); 85 } 86 onSearchClearButtonClicked()87 private void onSearchClearButtonClicked() { 88 searchBox.setText(""); 89 } 90 onSearchBackButtonClicked()91 private void onSearchBackButtonClicked() { 92 if (!isExpanded) { 93 return; 94 } 95 96 listener.onSearchBackButtonClicked(); 97 collapse(true); 98 } 99 voiceSearchClicked()100 private void voiceSearchClicked() { 101 listener.onVoiceButtonClicked( 102 result -> { 103 if (!TextUtils.isEmpty(result)) { 104 expand(/* animate */ true, Optional.of(result), /* requestFocus */ true); 105 } 106 }); 107 } 108 109 /** 110 * Expand the search bar and populate it with text if any exists. 111 * 112 * @param requestFocus should be false if showing the dialpad 113 */ expand(boolean animate, Optional<String> text, boolean requestFocus)114 /* package-private */ void expand(boolean animate, Optional<String> text, boolean requestFocus) { 115 if (isExpanded) { 116 return; 117 } 118 119 int duration = animate ? ANIMATION_DURATION : 0; 120 searchBoxExpanded.setVisibility(VISIBLE); 121 AnimUtils.crossFadeViews(searchBoxExpanded, searchBoxCollapsed, duration); 122 ValueAnimator animator = ValueAnimator.ofFloat(EXPAND_MARGIN_FRACTION_START, 0f); 123 animator.addUpdateListener(animation -> setMargins((Float) animation.getAnimatedValue())); 124 animator.setDuration(duration); 125 animator.addListener( 126 new AnimatorListenerAdapter() { 127 @Override 128 public void onAnimationStart(Animator animation) { 129 super.onAnimationStart(animation); 130 DialerUtils.showInputMethod(searchBox); 131 isExpanded = true; 132 } 133 134 @Override 135 public void onAnimationEnd(Animator animation) { 136 super.onAnimationEnd(animation); 137 if (text.isPresent()) { 138 searchBox.setText(text.get()); 139 } 140 // Don't request focus unless we're actually showing the search box, otherwise 141 // physical/bluetooth keyboards will type into this box when the dialpad is open. 142 if (requestFocus) { 143 searchBox.requestFocus(); 144 } 145 setBackgroundResource(R.drawable.search_bar_background); 146 } 147 }); 148 animator.start(); 149 } 150 151 /** Collapse the search bar and clear it's text. */ collapse(boolean animate)152 /* package-private */ void collapse(boolean animate) { 153 if (!isExpanded) { 154 return; 155 } 156 157 int duration = animate ? ANIMATION_DURATION : 0; 158 AnimUtils.crossFadeViews(searchBoxCollapsed, searchBoxExpanded, duration); 159 ValueAnimator animator = ValueAnimator.ofFloat(0f, EXPAND_MARGIN_FRACTION_START); 160 animator.addUpdateListener(animation -> setMargins((Float) animation.getAnimatedValue())); 161 animator.setDuration(duration); 162 163 animator.addListener( 164 new AnimatorListenerAdapter() { 165 @Override 166 public void onAnimationStart(Animator animation) { 167 super.onAnimationStart(animation); 168 DialerUtils.hideInputMethod(searchBox); 169 isExpanded = false; 170 } 171 172 @Override 173 public void onAnimationEnd(Animator animation) { 174 super.onAnimationEnd(animation); 175 searchBox.setText(""); 176 searchBoxExpanded.setVisibility(INVISIBLE); 177 setBackgroundResource(R.drawable.search_bar_background_rounded_corners); 178 } 179 }); 180 animator.start(); 181 } 182 183 /** 184 * Assigns margins to the search box as a fraction of its maximum margin size 185 * 186 * @param fraction How large the margins should be as a fraction of their full size 187 */ setMargins(float fraction)188 private void setMargins(float fraction) { 189 int margin = (int) (this.margin * fraction); 190 MarginLayoutParams params = (MarginLayoutParams) getLayoutParams(); 191 params.topMargin = margin; 192 params.bottomMargin = margin; 193 params.leftMargin = margin; 194 params.rightMargin = margin; 195 searchBoxExpanded.getLayoutParams().height = 196 (int) (animationEndHeight - (animationEndHeight - animationStartHeight) * fraction); 197 } 198 setSearchBarListener(@onNull SearchBarListener listener)199 /* package-private */ void setSearchBarListener(@NonNull SearchBarListener listener) { 200 this.listener = Assert.isNotNull(listener); 201 } 202 getQuery()203 public String getQuery() { 204 return searchBox.getText().toString(); 205 } 206 isExpanded()207 public boolean isExpanded() { 208 return isExpanded; 209 } 210 setQueryWithoutUpdate(String query)211 public void setQueryWithoutUpdate(String query) { 212 skipLatestTextChange = true; 213 searchBox.setText(query); 214 searchBox.setSelection(searchBox.getText().length()); 215 } 216 hideKeyboard()217 public void hideKeyboard() { 218 UiUtil.hideKeyboardFrom(getContext(), searchBox); 219 } 220 showKeyboard()221 public void showKeyboard() { 222 UiUtil.forceOpenKeyboardFrom(getContext(), searchBox); 223 } 224 setHint(@tringRes int hint)225 public void setHint(@StringRes int hint) { 226 searchBox.setHint(hint); 227 searchBoxTextView.setText(hint); 228 } 229 230 /** Handles logic for text changes in the search box. */ 231 private class SearchBoxTextWatcher implements TextWatcher { 232 233 @Override beforeTextChanged(CharSequence s, int start, int count, int after)234 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 235 236 @Override onTextChanged(CharSequence s, int start, int before, int count)237 public void onTextChanged(CharSequence s, int start, int before, int count) {} 238 239 @Override afterTextChanged(Editable s)240 public void afterTextChanged(Editable s) { 241 clearButton.setVisibility(TextUtils.isEmpty(s) ? GONE : VISIBLE); 242 if (skipLatestTextChange) { 243 skipLatestTextChange = false; 244 return; 245 } 246 247 // afterTextChanged is called each time the device is rotated (or the activity is recreated). 248 // That means that this method could potentially be called before the listener is set and 249 // we should check if it's null. In the case that it is null, assert that the query is empty 250 // because the listener must be notified of non-empty queries. 251 if (listener != null) { 252 listener.onSearchQueryUpdated(s.toString()); 253 } else { 254 Assert.checkArgument(TextUtils.isEmpty(s.toString())); 255 } 256 } 257 } 258 } 259