CallCardFragment.java revision d73f66fcfdad420b138565f38050af153edab6e8
1/* 2 * Copyright (C) 2013 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.incallui; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.AnimatorSet; 22import android.animation.ObjectAnimator; 23import android.app.Activity; 24import android.content.Context; 25import android.content.res.Configuration; 26import android.graphics.Point; 27import android.graphics.drawable.Drawable; 28import android.os.Bundle; 29import android.telephony.DisconnectCause; 30import android.telephony.PhoneNumberUtils; 31import android.text.TextUtils; 32import android.view.Display; 33import android.view.LayoutInflater; 34import android.view.View; 35import android.view.ViewAnimationUtils; 36import android.view.ViewGroup; 37import android.view.ViewTreeObserver; 38import android.view.ViewTreeObserver.OnGlobalLayoutListener; 39import android.view.accessibility.AccessibilityEvent; 40import android.view.animation.Animation; 41import android.view.animation.AnimationUtils; 42import android.widget.ImageButton; 43import android.widget.ImageView; 44import android.widget.TextView; 45 46import com.android.contacts.common.widget.FloatingActionButtonController; 47import com.android.phone.common.animation.AnimUtils; 48 49import java.util.List; 50 51/** 52 * Fragment for call card. 53 */ 54public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPresenter.CallCardUi> 55 implements CallCardPresenter.CallCardUi { 56 57 private int mRevealAnimationDuration; 58 private int mShrinkAnimationDuration; 59 private boolean mIsLandscape; 60 61 // Primary caller info 62 private TextView mPhoneNumber; 63 private TextView mNumberLabel; 64 private TextView mPrimaryName; 65 private View mCallStateButton; 66 private ImageView mCallStateIcon; 67 private TextView mCallStateLabel; 68 private TextView mCallTypeLabel; 69 private View mCallNumberAndLabel; 70 private ImageView mPhoto; 71 private TextView mElapsedTime; 72 73 // Container view that houses the entire primary call card, including the call buttons 74 private View mPrimaryCallCardContainer; 75 // Container view that houses the primary call information 76 private View mPrimaryCallInfo; 77 private View mCallButtonsContainer; 78 79 // Secondary caller info 80 private View mSecondaryCallInfo; 81 private TextView mSecondaryCallName; 82 private View mSecondaryCallProviderInfo; 83 private TextView mSecondaryCallProviderLabel; 84 private ImageView mSecondaryCallProviderIcon; 85 86 // Dark number info bar 87 private TextView mInCallMessageLabel; 88 89 private FloatingActionButtonController mFloatingActionButtonController; 90 private View mFloatingActionButtonContainer; 91 private int mFloatingActionButtonHideOffset; 92 93 // Cached DisplayMetrics density. 94 private float mDensity; 95 96 private float mTranslationOffset; 97 private Animation mPulseAnimation; 98 99 @Override 100 CallCardPresenter.CallCardUi getUi() { 101 return this; 102 } 103 104 @Override 105 CallCardPresenter createPresenter() { 106 return new CallCardPresenter(); 107 } 108 109 @Override 110 public void onCreate(Bundle savedInstanceState) { 111 super.onCreate(savedInstanceState); 112 113 mRevealAnimationDuration = getResources().getInteger(R.integer.reveal_animation_duration); 114 mShrinkAnimationDuration = getResources().getInteger(R.integer.shrink_animation_duration); 115 mFloatingActionButtonHideOffset = getResources().getDimensionPixelOffset( 116 R.dimen.end_call_button_hide_offset); 117 mIsLandscape = getResources().getConfiguration().orientation 118 == Configuration.ORIENTATION_LANDSCAPE; 119 } 120 121 122 @Override 123 public void onActivityCreated(Bundle savedInstanceState) { 124 super.onActivityCreated(savedInstanceState); 125 126 final CallList calls = CallList.getInstance(); 127 final Call call = calls.getFirstCall(); 128 getPresenter().init(getActivity(), call); 129 } 130 131 @Override 132 public View onCreateView(LayoutInflater inflater, ViewGroup container, 133 Bundle savedInstanceState) { 134 super.onCreateView(inflater, container, savedInstanceState); 135 136 mDensity = getResources().getDisplayMetrics().density; 137 mTranslationOffset = 138 getResources().getDimensionPixelSize(R.dimen.call_card_anim_translate_y_offset); 139 140 return inflater.inflate(R.layout.call_card, container, false); 141 } 142 143 @Override 144 public void onViewCreated(View view, Bundle savedInstanceState) { 145 super.onViewCreated(view, savedInstanceState); 146 147 mPulseAnimation = 148 AnimationUtils.loadAnimation(view.getContext(), R.anim.call_status_pulse); 149 150 mPhoneNumber = (TextView) view.findViewById(R.id.phoneNumber); 151 mPrimaryName = (TextView) view.findViewById(R.id.name); 152 mNumberLabel = (TextView) view.findViewById(R.id.label); 153 mSecondaryCallInfo = (View) view.findViewById(R.id.secondary_call_info); 154 mSecondaryCallProviderInfo = (View) view.findViewById(R.id.secondary_call_provider_info); 155 mPhoto = (ImageView) view.findViewById(R.id.photo); 156 mCallStateButton = view.findViewById(R.id.callStateButton); 157 mCallStateIcon = (ImageView) view.findViewById(R.id.callStateIcon); 158 mCallStateLabel = (TextView) view.findViewById(R.id.callStateLabel); 159 mCallNumberAndLabel = view.findViewById(R.id.labelAndNumber); 160 mCallTypeLabel = (TextView) view.findViewById(R.id.callTypeLabel); 161 mElapsedTime = (TextView) view.findViewById(R.id.elapsedTime); 162 mPrimaryCallCardContainer = view.findViewById(R.id.primary_call_info_container); 163 mPrimaryCallInfo = view.findViewById(R.id.primary_call_banner); 164 mCallButtonsContainer = view.findViewById(R.id.callButtonFragment); 165 mInCallMessageLabel = (TextView) view.findViewById(R.id.connectionServiceMessage); 166 167 mFloatingActionButtonContainer = view.findViewById( 168 R.id.floating_end_call_action_button_container); 169 ImageButton floatingActionButton = (ImageButton) view.findViewById( 170 R.id.floating_end_call_action_button); 171 floatingActionButton.setOnClickListener(new View.OnClickListener() { 172 @Override 173 public void onClick(View v) { 174 getPresenter().endCallClicked(); 175 } 176 }); 177 int floatingActionButtonWidth = getResources().getDimensionPixelSize( 178 R.dimen.floating_action_button_width); 179 mFloatingActionButtonController = new FloatingActionButtonController(getActivity(), 180 mFloatingActionButtonContainer); 181 final ViewGroup parent = (ViewGroup) mPrimaryCallCardContainer.getParent(); 182 final ViewTreeObserver observer = getView().getViewTreeObserver(); 183 observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { 184 @Override 185 public void onGlobalLayout() { 186 final ViewTreeObserver observer = getView().getViewTreeObserver(); 187 if (!observer.isAlive()) { 188 return; 189 } 190 observer.removeOnGlobalLayoutListener(this); 191 mFloatingActionButtonController.setScreenWidth(parent.getWidth()); 192 mFloatingActionButtonController.align( 193 mIsLandscape ? FloatingActionButtonController.ALIGN_QUARTER_RIGHT 194 : FloatingActionButtonController.ALIGN_MIDDLE, 195 0 /* offsetX */, 196 0 /* offsetY */, 197 false); 198 } 199 }); 200 201 mPrimaryName.setElegantTextHeight(false); 202 mCallStateLabel.setElegantTextHeight(false); 203 204 mCallStateButton.setOnClickListener(new View.OnClickListener() { 205 @Override 206 public void onClick(View v) { 207 getPresenter().phoneAccountClicked(); 208 } 209 }); 210 } 211 212 @Override 213 public void setVisible(boolean on) { 214 if (on) { 215 getView().setVisibility(View.VISIBLE); 216 } else { 217 getView().setVisibility(View.INVISIBLE); 218 } 219 } 220 221 @Override 222 public void setPrimaryName(String name, boolean nameIsNumber) { 223 if (TextUtils.isEmpty(name)) { 224 mPrimaryName.setText(""); 225 } else { 226 mPrimaryName.setText(name); 227 228 // Set direction of the name field 229 int nameDirection = View.TEXT_DIRECTION_INHERIT; 230 if (nameIsNumber) { 231 nameDirection = View.TEXT_DIRECTION_LTR; 232 } 233 mPrimaryName.setTextDirection(nameDirection); 234 } 235 } 236 237 @Override 238 public void setPrimaryImage(Drawable image) { 239 if (image != null) { 240 setDrawableToImageView(mPhoto, image); 241 } 242 } 243 244 @Override 245 public void setPrimaryPhoneNumber(String number) { 246 // Set the number 247 if (TextUtils.isEmpty(number)) { 248 mPhoneNumber.setText(""); 249 mPhoneNumber.setVisibility(View.GONE); 250 } else { 251 mPhoneNumber.setText(number); 252 mPhoneNumber.setVisibility(View.VISIBLE); 253 mPhoneNumber.setTextDirection(View.TEXT_DIRECTION_LTR); 254 } 255 } 256 257 @Override 258 public void setPrimaryLabel(String label) { 259 if (!TextUtils.isEmpty(label)) { 260 mNumberLabel.setText(label); 261 mNumberLabel.setVisibility(View.VISIBLE); 262 } else { 263 mNumberLabel.setVisibility(View.GONE); 264 } 265 266 } 267 268 @Override 269 public void setPrimary(String number, String name, boolean nameIsNumber, String label, 270 Drawable photo, boolean isConference, boolean isGeneric, boolean isSipCall) { 271 Log.d(this, "Setting primary call"); 272 273 if (isConference) { 274 name = getConferenceString(isGeneric); 275 photo = getConferencePhoto(isGeneric); 276 nameIsNumber = false; 277 } 278 279 // set the name field. 280 setPrimaryName(name, nameIsNumber); 281 282 if (TextUtils.isEmpty(number) && TextUtils.isEmpty(label)) { 283 mCallNumberAndLabel.setVisibility(View.GONE); 284 } else { 285 mCallNumberAndLabel.setVisibility(View.VISIBLE); 286 } 287 288 setPrimaryPhoneNumber(number); 289 290 // Set the label (Mobile, Work, etc) 291 setPrimaryLabel(label); 292 293 showInternetCallLabel(isSipCall); 294 295 setDrawableToImageView(mPhoto, photo); 296 } 297 298 @Override 299 public void setSecondary(boolean show, String name, boolean nameIsNumber, String label, 300 String providerLabel, Drawable providerIcon, boolean isConference, boolean isGeneric) { 301 302 if (show) { 303 if (isConference) { 304 name = getConferenceString(isGeneric); 305 nameIsNumber = false; 306 } 307 308 boolean hasProvider = !TextUtils.isEmpty(providerLabel); 309 showAndInitializeSecondaryCallInfo(hasProvider); 310 311 mSecondaryCallName.setText(name); 312 if (hasProvider) { 313 mSecondaryCallProviderLabel.setText(providerLabel); 314 mSecondaryCallProviderIcon.setImageDrawable(providerIcon); 315 } 316 317 int nameDirection = View.TEXT_DIRECTION_INHERIT; 318 if (nameIsNumber) { 319 nameDirection = View.TEXT_DIRECTION_LTR; 320 } 321 mSecondaryCallName.setTextDirection(nameDirection); 322 } else { 323 mSecondaryCallInfo.setVisibility(View.GONE); 324 } 325 } 326 327 @Override 328 public void setCallState(int state, int cause, String connectionLabel, Drawable connectionIcon, 329 String gatewayNumber) { 330 boolean isGatewayCall = !TextUtils.isEmpty(gatewayNumber); 331 String callStateLabel = getCallStateLabelFromState( 332 state, cause, connectionLabel, isGatewayCall); 333 334 Log.v(this, "setCallState " + callStateLabel); 335 Log.v(this, "DisconnectCause " + DisconnectCause.toString(cause)); 336 Log.v(this, "gateway " + connectionLabel + gatewayNumber); 337 338 // Update the call state label and icon. 339 if (!TextUtils.isEmpty(callStateLabel)) { 340 mCallStateLabel.setText(callStateLabel); 341 mCallStateLabel.setAlpha(1); 342 mCallStateLabel.setVisibility(View.VISIBLE); 343 344 if (connectionIcon == null) { 345 mCallStateIcon.setVisibility(View.GONE); 346 } else { 347 mCallStateIcon.setVisibility(View.VISIBLE); 348 mCallStateIcon.setImageDrawable(connectionIcon); 349 } 350 351 if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED) { 352 mCallStateLabel.clearAnimation(); 353 } else { 354 mCallStateLabel.startAnimation(mPulseAnimation); 355 } 356 } else { 357 Animation callStateAnimation = mCallStateLabel.getAnimation(); 358 if (callStateAnimation != null) { 359 callStateAnimation.cancel(); 360 } 361 mCallStateLabel.setAlpha(0); 362 mCallStateLabel.setVisibility(View.GONE); 363 } 364 } 365 366 367 @Override 368 public void setCallDetails(android.telecomm.Call.Details details) { 369 } 370 371 @Override 372 public void setEmergencyCallbackNumber(String callbackNumber) { 373 if (TextUtils.isEmpty(callbackNumber)) { 374 mInCallMessageLabel.setVisibility(View.GONE); 375 return; 376 } 377 378 // TODO: The new Locale-specific methods don't seem to be working. Revisit this. 379 callbackNumber = PhoneNumberUtils.formatNumber(callbackNumber); 380 381 String text = getString(R.string.card_title_callback_number_emergency, callbackNumber); 382 mInCallMessageLabel.setText(text); 383 384 mInCallMessageLabel.setVisibility(View.VISIBLE); 385 } 386 387 private void showInternetCallLabel(boolean show) { 388 if (show) { 389 final String label = getView().getContext().getString( 390 R.string.incall_call_type_label_sip); 391 mCallTypeLabel.setVisibility(View.VISIBLE); 392 mCallTypeLabel.setText(label); 393 } else { 394 mCallTypeLabel.setVisibility(View.GONE); 395 } 396 } 397 398 @Override 399 public void setPrimaryCallElapsedTime(boolean show, String callTimeElapsed) { 400 if (show) { 401 if (mElapsedTime.getVisibility() != View.VISIBLE) { 402 AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION); 403 } 404 mElapsedTime.setText(callTimeElapsed); 405 } else { 406 // hide() animation has no effect if it is already hidden. 407 AnimUtils.fadeOut(mElapsedTime, AnimUtils.DEFAULT_DURATION); 408 } 409 } 410 411 private void setDrawableToImageView(ImageView view, Drawable photo) { 412 if (photo == null) { 413 photo = view.getResources().getDrawable(R.drawable.picture_unknown); 414 } 415 416 final Drawable current = view.getDrawable(); 417 if (current == null) { 418 view.setImageDrawable(photo); 419 AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION); 420 } else { 421 InCallAnimationUtils.startCrossFade(view, current, photo); 422 view.setVisibility(View.VISIBLE); 423 } 424 } 425 426 private String getConferenceString(boolean isGeneric) { 427 Log.v(this, "isGenericString: " + isGeneric); 428 final int resId = isGeneric ? R.string.card_title_in_call : R.string.card_title_conf_call; 429 return getView().getResources().getString(resId); 430 } 431 432 private Drawable getConferencePhoto(boolean isGeneric) { 433 Log.v(this, "isGenericPhoto: " + isGeneric); 434 final int resId = isGeneric ? R.drawable.picture_dialing : R.drawable.picture_conference; 435 return getView().getResources().getDrawable(resId); 436 } 437 438 /** 439 * Gets the call state label based on the state of the call and 440 * cause of disconnect 441 * 442 * Additional labels are applied as follows: 443 * 1. All outgoing calls with display "Calling via [Provider]" 444 * 2. Ongoing calls will display the name of the provider 445 * 2. Incoming calls will only display "Incoming via..." for accounts 446 */ 447 private String getCallStateLabelFromState(int state, int cause, String label, 448 boolean isGatewayCall) { 449 final Context context = getView().getContext(); 450 String callStateLabel = null; // Label to display as part of the call banner 451 452 boolean isSpecialCall = label != null; 453 boolean isAccount = isSpecialCall && !isGatewayCall; 454 455 switch (state) { 456 case Call.State.IDLE: 457 // "Call state" is meaningless in this state. 458 break; 459 case Call.State.ACTIVE: 460 // We normally don't show a "call state label" at all in this state 461 // (but we can use the call state label to display the provider name). 462 if (isAccount) { 463 callStateLabel = label; 464 } 465 break; 466 case Call.State.ONHOLD: 467 callStateLabel = context.getString(R.string.card_title_on_hold); 468 break; 469 case Call.State.DIALING: 470 if (isSpecialCall) { 471 callStateLabel = context.getString(R.string.calling_via_template, label); 472 } else { 473 callStateLabel = context.getString(R.string.card_title_dialing); 474 } 475 break; 476 case Call.State.REDIALING: 477 callStateLabel = context.getString(R.string.card_title_redialing); 478 break; 479 case Call.State.INCOMING: 480 case Call.State.CALL_WAITING: 481 if (isAccount) { 482 callStateLabel = context.getString(R.string.incoming_via_template, label); 483 } else { 484 callStateLabel = context.getString(R.string.card_title_incoming_call); 485 } 486 break; 487 case Call.State.DISCONNECTING: 488 // While in the DISCONNECTING state we display a "Hanging up" 489 // message in order to make the UI feel more responsive. (In 490 // GSM it's normal to see a delay of a couple of seconds while 491 // negotiating the disconnect with the network, so the "Hanging 492 // up" state at least lets the user know that we're doing 493 // something. This state is currently not used with CDMA.) 494 callStateLabel = context.getString(R.string.card_title_hanging_up); 495 break; 496 case Call.State.DISCONNECTED: 497 callStateLabel = getCallFailedString(cause); 498 break; 499 default: 500 Log.wtf(this, "updateCallStateWidgets: unexpected call: " + state); 501 } 502 return callStateLabel; 503 } 504 505 /** 506 * Maps the disconnect cause to a resource string. 507 * 508 * @param cause disconnect cause as defined in {@link DisconnectCause} 509 */ 510 private String getCallFailedString(int cause) { 511 int resID = R.string.card_title_call_ended; 512 513 // TODO: The card *title* should probably be "Call ended" in all 514 // cases, but if the DisconnectCause was an error condition we should 515 // probably also display the specific failure reason somewhere... 516 517 switch (cause) { 518 case DisconnectCause.BUSY: 519 resID = R.string.callFailed_userBusy; 520 break; 521 522 case DisconnectCause.CONGESTION: 523 resID = R.string.callFailed_congestion; 524 break; 525 526 case DisconnectCause.TIMED_OUT: 527 resID = R.string.callFailed_timedOut; 528 break; 529 530 case DisconnectCause.SERVER_UNREACHABLE: 531 resID = R.string.callFailed_server_unreachable; 532 break; 533 534 case DisconnectCause.NUMBER_UNREACHABLE: 535 resID = R.string.callFailed_number_unreachable; 536 break; 537 538 case DisconnectCause.INVALID_CREDENTIALS: 539 resID = R.string.callFailed_invalid_credentials; 540 break; 541 542 case DisconnectCause.SERVER_ERROR: 543 resID = R.string.callFailed_server_error; 544 break; 545 546 case DisconnectCause.OUT_OF_NETWORK: 547 resID = R.string.callFailed_out_of_network; 548 break; 549 550 case DisconnectCause.LOST_SIGNAL: 551 case DisconnectCause.CDMA_DROP: 552 resID = R.string.callFailed_noSignal; 553 break; 554 555 case DisconnectCause.LIMIT_EXCEEDED: 556 resID = R.string.callFailed_limitExceeded; 557 break; 558 559 case DisconnectCause.POWER_OFF: 560 resID = R.string.callFailed_powerOff; 561 break; 562 563 case DisconnectCause.ICC_ERROR: 564 resID = R.string.callFailed_simError; 565 break; 566 567 case DisconnectCause.OUT_OF_SERVICE: 568 resID = R.string.callFailed_outOfService; 569 break; 570 571 case DisconnectCause.INVALID_NUMBER: 572 case DisconnectCause.UNOBTAINABLE_NUMBER: 573 resID = R.string.callFailed_unobtainable_number; 574 break; 575 576 default: 577 resID = R.string.card_title_call_ended; 578 break; 579 } 580 return this.getView().getContext().getString(resID); 581 } 582 583 private void showAndInitializeSecondaryCallInfo(boolean hasProvider) { 584 mSecondaryCallInfo.setVisibility(View.VISIBLE); 585 586 // mSecondaryCallName is initialized here (vs. onViewCreated) because it is inaccessible 587 // until mSecondaryCallInfo is inflated in the call above. 588 if (mSecondaryCallName == null) { 589 mSecondaryCallName = (TextView) getView().findViewById(R.id.secondaryCallName); 590 if (hasProvider) { 591 mSecondaryCallProviderInfo.setVisibility(View.VISIBLE); 592 mSecondaryCallProviderLabel = (TextView) getView() 593 .findViewById(R.id.secondaryCallProviderLabel); 594 mSecondaryCallProviderIcon = (ImageView) getView() 595 .findViewById(R.id.secondaryCallProviderIcon); 596 } 597 } 598 mSecondaryCallInfo.setOnClickListener(new View.OnClickListener() { 599 @Override 600 public void onClick(View v) { 601 getPresenter().secondaryInfoClicked(); 602 } 603 }); 604 } 605 606 public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 607 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { 608 dispatchPopulateAccessibilityEvent(event, mPrimaryName); 609 dispatchPopulateAccessibilityEvent(event, mPhoneNumber); 610 return; 611 } 612 dispatchPopulateAccessibilityEvent(event, mCallStateLabel); 613 dispatchPopulateAccessibilityEvent(event, mPrimaryName); 614 dispatchPopulateAccessibilityEvent(event, mPhoneNumber); 615 dispatchPopulateAccessibilityEvent(event, mCallTypeLabel); 616 dispatchPopulateAccessibilityEvent(event, mSecondaryCallName); 617 dispatchPopulateAccessibilityEvent(event, mSecondaryCallProviderLabel); 618 619 return; 620 } 621 622 @Override 623 public void setEndCallButtonEnabled(boolean enabled) { 624 mFloatingActionButtonController.setVisible(enabled); 625 } 626 627 private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) { 628 if (view == null) return; 629 final List<CharSequence> eventText = event.getText(); 630 int size = eventText.size(); 631 view.dispatchPopulateAccessibilityEvent(event); 632 // if no text added write null to keep relative position 633 if (size == eventText.size()) { 634 eventText.add(null); 635 } 636 } 637 638 public void animateForNewOutgoingCall() { 639 final ViewGroup parent = (ViewGroup) mPrimaryCallCardContainer.getParent(); 640 641 final ViewTreeObserver observer = getView().getViewTreeObserver(); 642 observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { 643 @Override 644 public void onGlobalLayout() { 645 final ViewTreeObserver observer = getView().getViewTreeObserver(); 646 if (!observer.isAlive()) { 647 return; 648 } 649 observer.removeOnGlobalLayoutListener(this); 650 651 final int originalHeight = mPrimaryCallCardContainer.getHeight(); 652 final LayoutIgnoringListener listener = new LayoutIgnoringListener(); 653 mPrimaryCallCardContainer.addOnLayoutChangeListener(listener); 654 655 // Prepare the state of views before the circular reveal animation 656 mPrimaryCallCardContainer.setBottom(parent.getHeight()); 657 658 // Set up FAB. 659 mFloatingActionButtonController.setScreenWidth(parent.getWidth()); 660 // Move it below the screen. 661 mFloatingActionButtonController.manuallyTranslate( 662 mFloatingActionButtonController.getTranslationXForAlignment( 663 mIsLandscape ? FloatingActionButtonController.ALIGN_QUARTER_RIGHT 664 : FloatingActionButtonController.ALIGN_MIDDLE 665 ), 666 mFloatingActionButtonHideOffset 667 ); 668 mCallButtonsContainer.setAlpha(0); 669 mCallStateLabel.setAlpha(0); 670 mPrimaryName.setAlpha(0); 671 mCallTypeLabel.setAlpha(0); 672 mCallNumberAndLabel.setAlpha(0); 673 674 final Animator revealAnimator = getRevealAnimator(); 675 final Animator shrinkAnimator = 676 getShrinkAnimator(parent.getHeight(), originalHeight); 677 678 final AnimatorSet set = new AnimatorSet(); 679 set.playSequentially(revealAnimator, shrinkAnimator); 680 set.addListener(new AnimatorListenerAdapter() { 681 @Override 682 public void onAnimationCancel(Animator animation) { 683 mPrimaryCallCardContainer.removeOnLayoutChangeListener(listener); 684 } 685 686 @Override 687 public void onAnimationEnd(Animator animation) { 688 mPrimaryCallCardContainer.removeOnLayoutChangeListener(listener); 689 } 690 }); 691 set.start(); 692 } 693 }); 694 } 695 696 /** 697 * Animator that performs the upwards shrinking animation of the blue call card scrim. 698 * At the start of the animation, each child view is moved downwards by a pre-specified amount 699 * and then translated upwards together with the scrim. 700 */ 701 private Animator getShrinkAnimator(int startHeight, int endHeight) { 702 final Animator shrinkAnimator = 703 ObjectAnimator.ofInt(mPrimaryCallCardContainer, "bottom", 704 startHeight, endHeight); 705 shrinkAnimator.setDuration(mShrinkAnimationDuration); 706 shrinkAnimator.addListener(new AnimatorListenerAdapter() { 707 @Override 708 public void onAnimationStart(Animator animation) { 709 assignTranslateAnimation(mCallStateLabel, 1); 710 assignTranslateAnimation(mPrimaryName, 2); 711 assignTranslateAnimation(mCallNumberAndLabel, 3); 712 assignTranslateAnimation(mCallTypeLabel, 4); 713 assignTranslateAnimation(mCallButtonsContainer, 5); 714 715 mFloatingActionButtonController.align( 716 mIsLandscape ? FloatingActionButtonController.ALIGN_QUARTER_RIGHT 717 : FloatingActionButtonController.ALIGN_MIDDLE, 718 0 /* offsetX */, 719 0 /* offsetY */, 720 true); 721 } 722 }); 723 shrinkAnimator.setInterpolator(AnimUtils.EASE_IN); 724 return shrinkAnimator; 725 } 726 727 private Animator getRevealAnimator() { 728 final Activity activity = getActivity(); 729 final View view = activity.getWindow().getDecorView(); 730 final Display display = activity.getWindowManager().getDefaultDisplay(); 731 final Point size = new Point(); 732 display.getSize(size); 733 734 final Animator valueAnimator = ViewAnimationUtils.createCircularReveal(view, 735 size.x / 2, size.y / 2, 0, Math.max(size.x, size.y)); 736 valueAnimator.setDuration(mRevealAnimationDuration); 737 return valueAnimator; 738 } 739 740 private void assignTranslateAnimation(View view, int offset) { 741 view.setTranslationY(mTranslationOffset * offset); 742 view.animate().translationY(0).alpha(1).withLayer() 743 .setDuration(mShrinkAnimationDuration).setInterpolator(AnimUtils.EASE_IN); 744 } 745 746 private final class LayoutIgnoringListener implements View.OnLayoutChangeListener { 747 @Override 748 public void onLayoutChange(View v, 749 int left, 750 int top, 751 int right, 752 int bottom, 753 int oldLeft, 754 int oldTop, 755 int oldRight, 756 int oldBottom) { 757 v.setLeft(oldLeft); 758 v.setRight(oldRight); 759 v.setTop(oldTop); 760 v.setBottom(oldBottom); 761 } 762 } 763} 764