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.phone.common.dialpad; 18 19 import android.animation.AnimatorListenerAdapter; 20 import android.content.Context; 21 import android.content.res.ColorStateList; 22 import android.content.res.Configuration; 23 import android.content.res.Resources; 24 import android.content.res.TypedArray; 25 import android.graphics.drawable.RippleDrawable; 26 import android.text.TextUtils; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.view.MotionEvent; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.view.ViewPropertyAnimator; 33 import android.view.accessibility.AccessibilityManager; 34 import android.widget.EditText; 35 import android.widget.ImageButton; 36 import android.widget.LinearLayout; 37 import android.widget.TextView; 38 39 import com.android.phone.common.R; 40 import com.android.phone.common.animation.AnimUtils; 41 42 import java.util.Locale; 43 44 /** 45 * View that displays a twelve-key phone dialpad. 46 */ 47 public class DialpadView extends LinearLayout { 48 private static final String TAG = DialpadView.class.getSimpleName(); 49 50 private static final double DELAY_MULTIPLIER = 0.66; 51 private static final double DURATION_MULTIPLIER = 0.8; 52 53 /** 54 * {@code True} if the dialpad is in landscape orientation. 55 */ 56 private final boolean mIsLandscape; 57 58 /** 59 * {@code True} if the dialpad is showing in a right-to-left locale. 60 */ 61 private final boolean mIsRtl; 62 63 private EditText mDigits; 64 private ImageButton mDelete; 65 private View mOverflowMenuButton; 66 private ColorStateList mRippleColor; 67 68 private ViewGroup mRateContainer; 69 private TextView mIldCountry; 70 private TextView mIldRate; 71 72 private boolean mCanDigitsBeEdited; 73 74 private final int[] mButtonIds = new int[] {R.id.zero, R.id.one, R.id.two, R.id.three, 75 R.id.four, R.id.five, R.id.six, R.id.seven, R.id.eight, R.id.nine, R.id.star, 76 R.id.pound}; 77 78 // For animation. 79 private static final int KEY_FRAME_DURATION = 33; 80 81 private int mTranslateDistance; 82 DialpadView(Context context)83 public DialpadView(Context context) { 84 this(context, null); 85 } 86 DialpadView(Context context, AttributeSet attrs)87 public DialpadView(Context context, AttributeSet attrs) { 88 this(context, attrs, 0); 89 } 90 DialpadView(Context context, AttributeSet attrs, int defStyle)91 public DialpadView(Context context, AttributeSet attrs, int defStyle) { 92 super(context, attrs, defStyle); 93 94 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Dialpad); 95 mRippleColor = a.getColorStateList(R.styleable.Dialpad_dialpad_key_button_touch_tint); 96 a.recycle(); 97 98 mTranslateDistance = getResources().getDimensionPixelSize( 99 R.dimen.dialpad_key_button_translate_y); 100 101 mIsLandscape = getResources().getConfiguration().orientation == 102 Configuration.ORIENTATION_LANDSCAPE; 103 mIsRtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == 104 View.LAYOUT_DIRECTION_RTL; 105 } 106 107 @Override onFinishInflate()108 protected void onFinishInflate() { 109 setupKeypad(); 110 mDigits = (EditText) findViewById(R.id.digits); 111 mDelete = (ImageButton) findViewById(R.id.deleteButton); 112 mOverflowMenuButton = findViewById(R.id.dialpad_overflow); 113 mRateContainer = (ViewGroup) findViewById(R.id.rate_container); 114 mIldCountry = (TextView) mRateContainer.findViewById(R.id.ild_country); 115 mIldRate = (TextView) mRateContainer.findViewById(R.id.ild_rate); 116 117 AccessibilityManager accessibilityManager = (AccessibilityManager) 118 getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 119 if (accessibilityManager.isEnabled()) { 120 // The text view must be selected to send accessibility events. 121 mDigits.setSelected(true); 122 } 123 } 124 setupKeypad()125 private void setupKeypad() { 126 final int[] numberIds = new int[] {R.string.dialpad_0_number, R.string.dialpad_1_number, 127 R.string.dialpad_2_number, R.string.dialpad_3_number, R.string.dialpad_4_number, 128 R.string.dialpad_5_number, R.string.dialpad_6_number, R.string.dialpad_7_number, 129 R.string.dialpad_8_number, R.string.dialpad_9_number, R.string.dialpad_star_number, 130 R.string.dialpad_pound_number}; 131 132 final int[] letterIds = new int[] {R.string.dialpad_0_letters, R.string.dialpad_1_letters, 133 R.string.dialpad_2_letters, R.string.dialpad_3_letters, R.string.dialpad_4_letters, 134 R.string.dialpad_5_letters, R.string.dialpad_6_letters, R.string.dialpad_7_letters, 135 R.string.dialpad_8_letters, R.string.dialpad_9_letters, 136 R.string.dialpad_star_letters, R.string.dialpad_pound_letters}; 137 138 final Resources resources = getContext().getResources(); 139 140 DialpadKeyButton dialpadKey; 141 TextView numberView; 142 TextView lettersView; 143 144 for (int i = 0; i < mButtonIds.length; i++) { 145 dialpadKey = (DialpadKeyButton) findViewById(mButtonIds[i]); 146 numberView = (TextView) dialpadKey.findViewById(R.id.dialpad_key_number); 147 lettersView = (TextView) dialpadKey.findViewById(R.id.dialpad_key_letters); 148 final String numberString = resources.getString(numberIds[i]); 149 final RippleDrawable rippleBackground = 150 (RippleDrawable) getContext().getDrawable(R.drawable.btn_dialpad_key); 151 if (mRippleColor != null) { 152 rippleBackground.setColor(mRippleColor); 153 } 154 155 numberView.setText(numberString); 156 numberView.setElegantTextHeight(false); 157 dialpadKey.setContentDescription(numberString); 158 dialpadKey.setBackground(rippleBackground); 159 160 if (lettersView != null) { 161 lettersView.setText(resources.getString(letterIds[i])); 162 } 163 } 164 165 final DialpadKeyButton one = (DialpadKeyButton) findViewById(R.id.one); 166 one.setLongHoverContentDescription( 167 resources.getText(R.string.description_voicemail_button)); 168 169 final DialpadKeyButton zero = (DialpadKeyButton) findViewById(R.id.zero); 170 zero.setLongHoverContentDescription( 171 resources.getText(R.string.description_image_button_plus)); 172 173 } 174 setShowVoicemailButton(boolean show)175 public void setShowVoicemailButton(boolean show) { 176 View view = findViewById(R.id.dialpad_key_voicemail); 177 if (view != null) { 178 view.setVisibility(show ? View.VISIBLE : View.INVISIBLE); 179 } 180 } 181 182 /** 183 * Whether or not the digits above the dialer can be edited. 184 * 185 * @param canBeEdited If true, the backspace button will be shown and the digits EditText 186 * will be configured to allow text manipulation. 187 */ setCanDigitsBeEdited(boolean canBeEdited)188 public void setCanDigitsBeEdited(boolean canBeEdited) { 189 View deleteButton = findViewById(R.id.deleteButton); 190 deleteButton.setVisibility(canBeEdited ? View.VISIBLE : View.GONE); 191 View overflowMenuButton = findViewById(R.id.dialpad_overflow); 192 overflowMenuButton.setVisibility(canBeEdited ? View.VISIBLE : View.GONE); 193 194 EditText digits = (EditText) findViewById(R.id.digits); 195 digits.setClickable(canBeEdited); 196 digits.setLongClickable(canBeEdited); 197 digits.setFocusableInTouchMode(canBeEdited); 198 digits.setCursorVisible(false); 199 200 mCanDigitsBeEdited = canBeEdited; 201 } 202 setCallRateInformation(String countryName, String displayRate)203 public void setCallRateInformation(String countryName, String displayRate) { 204 if (TextUtils.isEmpty(countryName) && TextUtils.isEmpty(displayRate)) { 205 mRateContainer.setVisibility(View.GONE); 206 return; 207 } 208 mRateContainer.setVisibility(View.VISIBLE); 209 mIldCountry.setText(countryName); 210 mIldRate.setText(displayRate); 211 } 212 canDigitsBeEdited()213 public boolean canDigitsBeEdited() { 214 return mCanDigitsBeEdited; 215 } 216 217 /** 218 * Always returns true for onHoverEvent callbacks, to fix problems with accessibility due to 219 * the dialpad overlaying other fragments. 220 */ 221 @Override onHoverEvent(MotionEvent event)222 public boolean onHoverEvent(MotionEvent event) { 223 return true; 224 } 225 animateShow()226 public void animateShow() { 227 // This is a hack; without this, the setTranslationY is delayed in being applied, and the 228 // numbers appear at their original position (0) momentarily before animating. 229 final AnimatorListenerAdapter showListener = new AnimatorListenerAdapter() {}; 230 231 for (int i = 0; i < mButtonIds.length; i++) { 232 int delay = (int)(getKeyButtonAnimationDelay(mButtonIds[i]) * DELAY_MULTIPLIER); 233 int duration = 234 (int)(getKeyButtonAnimationDuration(mButtonIds[i]) * DURATION_MULTIPLIER); 235 final DialpadKeyButton dialpadKey = (DialpadKeyButton) findViewById(mButtonIds[i]); 236 237 ViewPropertyAnimator animator = dialpadKey.animate(); 238 if (mIsLandscape) { 239 // Landscape orientation requires translation along the X axis. 240 // For RTL locales, ensure we translate negative on the X axis. 241 dialpadKey.setTranslationX((mIsRtl ? -1 : 1) * mTranslateDistance); 242 animator.translationX(0); 243 } else { 244 // Portrait orientation requires translation along the Y axis. 245 dialpadKey.setTranslationY(mTranslateDistance); 246 animator.translationY(0); 247 } 248 animator.setInterpolator(AnimUtils.EASE_OUT_EASE_IN) 249 .setStartDelay(delay) 250 .setDuration(duration) 251 .setListener(showListener) 252 .start(); 253 } 254 } 255 getDigits()256 public EditText getDigits() { 257 return mDigits; 258 } 259 getDeleteButton()260 public ImageButton getDeleteButton() { 261 return mDelete; 262 } 263 getOverflowMenuButton()264 public View getOverflowMenuButton() { 265 return mOverflowMenuButton; 266 } 267 268 /** 269 * Get the animation delay for the buttons, taking into account whether the dialpad is in 270 * landscape left-to-right, landscape right-to-left, or portrait. 271 * 272 * @param buttonId The button ID. 273 * @return The animation delay. 274 */ getKeyButtonAnimationDelay(int buttonId)275 private int getKeyButtonAnimationDelay(int buttonId) { 276 if (mIsLandscape) { 277 if (mIsRtl) { 278 switch (buttonId) { 279 case R.id.three: return KEY_FRAME_DURATION * 1; 280 case R.id.six: return KEY_FRAME_DURATION * 2; 281 case R.id.nine: return KEY_FRAME_DURATION * 3; 282 case R.id.pound: return KEY_FRAME_DURATION * 4; 283 case R.id.two: return KEY_FRAME_DURATION * 5; 284 case R.id.five: return KEY_FRAME_DURATION * 6; 285 case R.id.eight: return KEY_FRAME_DURATION * 7; 286 case R.id.zero: return KEY_FRAME_DURATION * 8; 287 case R.id.one: return KEY_FRAME_DURATION * 9; 288 case R.id.four: return KEY_FRAME_DURATION * 10; 289 case R.id.seven: 290 case R.id.star: 291 return KEY_FRAME_DURATION * 11; 292 } 293 } else { 294 switch (buttonId) { 295 case R.id.one: return KEY_FRAME_DURATION * 1; 296 case R.id.four: return KEY_FRAME_DURATION * 2; 297 case R.id.seven: return KEY_FRAME_DURATION * 3; 298 case R.id.star: return KEY_FRAME_DURATION * 4; 299 case R.id.two: return KEY_FRAME_DURATION * 5; 300 case R.id.five: return KEY_FRAME_DURATION * 6; 301 case R.id.eight: return KEY_FRAME_DURATION * 7; 302 case R.id.zero: return KEY_FRAME_DURATION * 8; 303 case R.id.three: return KEY_FRAME_DURATION * 9; 304 case R.id.six: return KEY_FRAME_DURATION * 10; 305 case R.id.nine: 306 case R.id.pound: 307 return KEY_FRAME_DURATION * 11; 308 } 309 } 310 } else { 311 switch (buttonId) { 312 case R.id.one: return KEY_FRAME_DURATION * 1; 313 case R.id.two: return KEY_FRAME_DURATION * 2; 314 case R.id.three: return KEY_FRAME_DURATION * 3; 315 case R.id.four: return KEY_FRAME_DURATION * 4; 316 case R.id.five: return KEY_FRAME_DURATION * 5; 317 case R.id.six: return KEY_FRAME_DURATION * 6; 318 case R.id.seven: return KEY_FRAME_DURATION * 7; 319 case R.id.eight: return KEY_FRAME_DURATION * 8; 320 case R.id.nine: return KEY_FRAME_DURATION * 9; 321 case R.id.star: return KEY_FRAME_DURATION * 10; 322 case R.id.zero: 323 case R.id.pound: 324 return KEY_FRAME_DURATION * 11; 325 } 326 } 327 328 Log.wtf(TAG, "Attempted to get animation delay for invalid key button id."); 329 return 0; 330 } 331 332 /** 333 * Get the button animation duration, taking into account whether the dialpad is in landscape 334 * left-to-right, landscape right-to-left, or portrait. 335 * 336 * @param buttonId The button ID. 337 * @return The animation duration. 338 */ getKeyButtonAnimationDuration(int buttonId)339 private int getKeyButtonAnimationDuration(int buttonId) { 340 if (mIsLandscape) { 341 if (mIsRtl) { 342 switch (buttonId) { 343 case R.id.one: 344 case R.id.four: 345 case R.id.seven: 346 case R.id.star: 347 return KEY_FRAME_DURATION * 8; 348 case R.id.two: 349 case R.id.five: 350 case R.id.eight: 351 case R.id.zero: 352 return KEY_FRAME_DURATION * 9; 353 case R.id.three: 354 case R.id.six: 355 case R.id.nine: 356 case R.id.pound: 357 return KEY_FRAME_DURATION * 10; 358 } 359 } else { 360 switch (buttonId) { 361 case R.id.one: 362 case R.id.four: 363 case R.id.seven: 364 case R.id.star: 365 return KEY_FRAME_DURATION * 10; 366 case R.id.two: 367 case R.id.five: 368 case R.id.eight: 369 case R.id.zero: 370 return KEY_FRAME_DURATION * 9; 371 case R.id.three: 372 case R.id.six: 373 case R.id.nine: 374 case R.id.pound: 375 return KEY_FRAME_DURATION * 8; 376 } 377 } 378 } else { 379 switch (buttonId) { 380 case R.id.one: 381 case R.id.two: 382 case R.id.three: 383 case R.id.four: 384 case R.id.five: 385 case R.id.six: 386 return KEY_FRAME_DURATION * 10; 387 case R.id.seven: 388 case R.id.eight: 389 case R.id.nine: 390 return KEY_FRAME_DURATION * 9; 391 case R.id.star: 392 case R.id.zero: 393 case R.id.pound: 394 return KEY_FRAME_DURATION * 8; 395 } 396 } 397 398 Log.wtf(TAG, "Attempted to get animation duration for invalid key button id."); 399 return 0; 400 } 401 } 402