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