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