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