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