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