CallCardFragment.java revision a2694eb9f05b8fc1f954c35a0bfd1acd1e9376fa
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            mCallStateLabel.getAnimation().cancel();
309            mCallStateLabel.setAlpha(0);
310            mCallStateLabel.setVisibility(View.GONE);
311        }
312
313        if (Call.State.INCOMING == state) {
314            setBluetoothOn(bluetoothOn);
315        }
316
317        mHandoffButton.setEnabled(isHandoffCapable && !isHandoffPending);
318        mHandoffButton.setVisibility(isWiFi || mHandoffButton.isEnabled() ?
319                View.VISIBLE : View.GONE);
320        mHandoffButton.setImageResource(isWiFi ?
321                R.drawable.ic_in_call_wifi : R.drawable.ic_in_call_pstn);
322    }
323
324    private void showInternetCallLabel(boolean show) {
325        if (show) {
326            final String label = getView().getContext().getString(
327                    R.string.incall_call_type_label_sip);
328            mCallTypeLabel.setVisibility(View.VISIBLE);
329            mCallTypeLabel.setText(label);
330        } else {
331            mCallTypeLabel.setVisibility(View.GONE);
332        }
333    }
334
335    @Override
336    public void setPrimaryCallElapsedTime(boolean show, String callTimeElapsed) {
337        if (show) {
338            if (mElapsedTime.getVisibility() != View.VISIBLE) {
339                AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION);
340            }
341            mElapsedTime.setText(callTimeElapsed);
342        } else {
343            // hide() animation has no effect if it is already hidden.
344            AnimUtils.fadeOut(mElapsedTime, AnimUtils.DEFAULT_DURATION);
345        }
346    }
347
348    private void setDrawableToImageView(ImageView view, Drawable photo) {
349        if (photo == null) {
350            photo = view.getResources().getDrawable(R.drawable.picture_unknown);
351        }
352
353        final Drawable current = view.getDrawable();
354        if (current == null) {
355            view.setImageDrawable(photo);
356            AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION);
357        } else {
358            InCallAnimationUtils.startCrossFade(view, current, photo);
359            view.setVisibility(View.VISIBLE);
360        }
361    }
362
363    private String getConferenceString(boolean isGeneric) {
364        Log.v(this, "isGenericString: " + isGeneric);
365        final int resId = isGeneric ? R.string.card_title_in_call : R.string.card_title_conf_call;
366        return getView().getResources().getString(resId);
367    }
368
369    private Drawable getConferencePhoto(boolean isGeneric) {
370        Log.v(this, "isGenericPhoto: " + isGeneric);
371        final int resId = isGeneric ? R.drawable.picture_dialing : R.drawable.picture_conference;
372        return getView().getResources().getDrawable(resId);
373    }
374
375    private void setBluetoothOn(boolean onOff) {
376        // Also, display a special icon (alongside the "Incoming call"
377        // label) if there's an incoming call and audio will be routed
378        // to bluetooth when you answer it.
379        final int bluetoothIconId = R.drawable.ic_in_call_bt_dk;
380
381        if (onOff) {
382            mCallStateLabel.setCompoundDrawablesWithIntrinsicBounds(bluetoothIconId, 0, 0, 0);
383            mCallStateLabel.setCompoundDrawablePadding((int) (mDensity * 5));
384        } else {
385            // Clear out any icons
386            mCallStateLabel.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
387        }
388    }
389
390    /**
391     * Gets the call state label based on the state of the call and
392     * cause of disconnect
393     */
394    private String getCallStateLabelFromState(int state, int cause) {
395        final Context context = getView().getContext();
396        String callStateLabel = null;  // Label to display as part of the call banner
397
398        if (Call.State.IDLE == state) {
399            // "Call state" is meaningless in this state.
400
401        } else if (Call.State.ACTIVE == state) {
402            // We normally don't show a "call state label" at all in
403            // this state (but see below for some special cases).
404
405        } else if (Call.State.ONHOLD == state) {
406            callStateLabel = context.getString(R.string.card_title_on_hold);
407        } else if (Call.State.DIALING == state) {
408            callStateLabel = context.getString(R.string.card_title_dialing);
409        } else if (Call.State.REDIALING == state) {
410            callStateLabel = context.getString(R.string.card_title_redialing);
411        } else if (Call.State.INCOMING == state || Call.State.CALL_WAITING == state) {
412            callStateLabel = context.getString(R.string.card_title_incoming_call);
413
414        } else if (Call.State.DISCONNECTING == state) {
415            // While in the DISCONNECTING state we display a "Hanging up"
416            // message in order to make the UI feel more responsive.  (In
417            // GSM it's normal to see a delay of a couple of seconds while
418            // negotiating the disconnect with the network, so the "Hanging
419            // up" state at least lets the user know that we're doing
420            // something.  This state is currently not used with CDMA.)
421            callStateLabel = context.getString(R.string.card_title_hanging_up);
422
423        } else if (Call.State.DISCONNECTED == state) {
424            callStateLabel = getCallFailedString(cause);
425
426        } else {
427            Log.wtf(this, "updateCallStateWidgets: unexpected call: " + state);
428        }
429
430        return callStateLabel;
431    }
432
433    /**
434     * Maps the disconnect cause to a resource string.
435     *
436     * @param cause disconnect cause as defined in {@link DisconnectCause}
437     */
438    private String getCallFailedString(int cause) {
439        int resID = R.string.card_title_call_ended;
440
441        // TODO: The card *title* should probably be "Call ended" in all
442        // cases, but if the DisconnectCause was an error condition we should
443        // probably also display the specific failure reason somewhere...
444
445        switch (cause) {
446            case DisconnectCause.BUSY:
447                resID = R.string.callFailed_userBusy;
448                break;
449
450            case DisconnectCause.CONGESTION:
451                resID = R.string.callFailed_congestion;
452                break;
453
454            case DisconnectCause.TIMED_OUT:
455                resID = R.string.callFailed_timedOut;
456                break;
457
458            case DisconnectCause.SERVER_UNREACHABLE:
459                resID = R.string.callFailed_server_unreachable;
460                break;
461
462            case DisconnectCause.NUMBER_UNREACHABLE:
463                resID = R.string.callFailed_number_unreachable;
464                break;
465
466            case DisconnectCause.INVALID_CREDENTIALS:
467                resID = R.string.callFailed_invalid_credentials;
468                break;
469
470            case DisconnectCause.SERVER_ERROR:
471                resID = R.string.callFailed_server_error;
472                break;
473
474            case DisconnectCause.OUT_OF_NETWORK:
475                resID = R.string.callFailed_out_of_network;
476                break;
477
478            case DisconnectCause.LOST_SIGNAL:
479            case DisconnectCause.CDMA_DROP:
480                resID = R.string.callFailed_noSignal;
481                break;
482
483            case DisconnectCause.LIMIT_EXCEEDED:
484                resID = R.string.callFailed_limitExceeded;
485                break;
486
487            case DisconnectCause.POWER_OFF:
488                resID = R.string.callFailed_powerOff;
489                break;
490
491            case DisconnectCause.ICC_ERROR:
492                resID = R.string.callFailed_simError;
493                break;
494
495            case DisconnectCause.OUT_OF_SERVICE:
496                resID = R.string.callFailed_outOfService;
497                break;
498
499            case DisconnectCause.INVALID_NUMBER:
500            case DisconnectCause.UNOBTAINABLE_NUMBER:
501                resID = R.string.callFailed_unobtainable_number;
502                break;
503
504            default:
505                resID = R.string.card_title_call_ended;
506                break;
507        }
508        return this.getView().getContext().getString(resID);
509    }
510
511    private void showAndInitializeSecondaryCallInfo() {
512        mSecondaryCallInfo.setVisibility(View.VISIBLE);
513
514        // mSecondaryCallName is initialized here (vs. onViewCreated) because it is inaccesible
515        // until mSecondaryCallInfo is inflated in the call above.
516        if (mSecondaryCallName == null) {
517            mSecondaryCallName = (TextView) getView().findViewById(R.id.secondaryCallName);
518        }
519        mSecondaryCallInfo.setOnClickListener(new View.OnClickListener() {
520            @Override
521            public void onClick(View v) {
522                getPresenter().secondaryInfoClicked();
523            }
524        });
525    }
526
527    public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
528        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
529            dispatchPopulateAccessibilityEvent(event, mPrimaryName);
530            dispatchPopulateAccessibilityEvent(event, mPhoneNumber);
531            return;
532        }
533        dispatchPopulateAccessibilityEvent(event, mCallStateLabel);
534        dispatchPopulateAccessibilityEvent(event, mPrimaryName);
535        dispatchPopulateAccessibilityEvent(event, mPhoneNumber);
536        dispatchPopulateAccessibilityEvent(event, mCallTypeLabel);
537        dispatchPopulateAccessibilityEvent(event, mSecondaryCallName);
538
539        return;
540    }
541
542    @Override
543    public void setEndCallButtonEnabled(boolean enabled) {
544        mEndCallButton.setVisibility(enabled ? View.VISIBLE : View.GONE);
545    }
546
547    private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) {
548        if (view == null) return;
549        final List<CharSequence> eventText = event.getText();
550        int size = eventText.size();
551        view.dispatchPopulateAccessibilityEvent(event);
552        // if no text added write null to keep relative position
553        if (size == eventText.size()) {
554            eventText.add(null);
555        }
556    }
557
558    public void animateForNewOutgoingCall() {
559        final ViewGroup parent = (ViewGroup) mPrimaryCallCardContainer.getParent();
560
561        final ViewTreeObserver observer = getView().getViewTreeObserver();
562        observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
563            @Override
564            public void onGlobalLayout() {
565                final ViewTreeObserver observer = getView().getViewTreeObserver();
566                if (!observer.isAlive()) {
567                    return;
568                }
569                observer.removeOnGlobalLayoutListener(this);
570
571                final int originalHeight = mPrimaryCallCardContainer.getHeight();
572                final LayoutIgnoringListener listener = new LayoutIgnoringListener();
573                mPrimaryCallCardContainer.addOnLayoutChangeListener(listener);
574
575                // Prepare the state of views before the circular reveal animation
576                mPrimaryCallCardContainer.setBottom(parent.getHeight());
577                mEndCallButton.setTranslationY(200);
578                mCallButtonsContainer.setAlpha(0);
579                mCallStateLabel.setAlpha(0);
580                mPrimaryName.setAlpha(0);
581                mCallTypeLabel.setAlpha(0);
582                mCallNumberAndLabel.setAlpha(0);
583
584                final Animator revealAnimator = getRevealAnimator();
585                final Animator shrinkAnimator =
586                        getShrinkAnimator(parent.getHeight(), originalHeight);
587
588                final AnimatorSet set = new AnimatorSet();
589                set.playSequentially(revealAnimator, shrinkAnimator);
590                set.addListener(new AnimatorListenerAdapter() {
591                    @Override
592                    public void onAnimationCancel(Animator animation) {
593                        mPrimaryCallCardContainer.removeOnLayoutChangeListener(listener);
594                    }
595
596                    @Override
597                    public void onAnimationEnd(Animator animation) {
598                        mPrimaryCallCardContainer.removeOnLayoutChangeListener(listener);
599                    }
600                });
601                set.start();
602            }
603        });
604    }
605
606    /**
607     * Animator that performs the upwards shrinking animation of the blue call card scrim.
608     * At the start of the animation, each child view is moved downwards by a pre-specified amount
609     * and then translated upwards together with the scrim.
610     */
611    private Animator getShrinkAnimator(int startHeight, int endHeight) {
612        final Animator shrinkAnimator =
613                ObjectAnimator.ofInt(mPrimaryCallCardContainer, "bottom",
614                        startHeight, endHeight);
615        shrinkAnimator.setDuration(SHRINK_ANIMATION_DURATION);
616        shrinkAnimator.addListener(new AnimatorListenerAdapter() {
617            @Override
618            public void onAnimationStart(Animator animation) {
619                assignTranslateAnimation(mCallStateLabel, 1);
620                assignTranslateAnimation(mPrimaryName, 2);
621                assignTranslateAnimation(mCallNumberAndLabel, 3);
622                assignTranslateAnimation(mCallTypeLabel, 4);
623                assignTranslateAnimation(mCallButtonsContainer, 5);
624
625                mEndCallButton.animate().translationY(0)
626                        .setDuration(SHRINK_ANIMATION_DURATION);
627            }
628        });
629        shrinkAnimator.setInterpolator(AnimUtils.EASE_IN);
630        return shrinkAnimator;
631    }
632
633    private Animator getRevealAnimator() {
634        final Activity activity = getActivity();
635        final View view  = activity.getWindow().getDecorView();
636        final Display display = activity.getWindowManager().getDefaultDisplay();
637        final Point size = new Point();
638        display.getSize(size);
639
640        final ValueAnimator valueAnimator = ViewAnimationUtils.createCircularReveal(view,
641                size.x / 2, size.y / 2, 0, Math.max(size.x, size.y));
642        valueAnimator.setDuration(REVEAL_ANIMATION_DURATION);
643        return valueAnimator;
644    }
645
646    private void assignTranslateAnimation(View view, int offset) {
647        view.setTranslationY(mTranslationOffset * offset);
648        view.animate().translationY(0).alpha(1).withLayer()
649                .setDuration(SHRINK_ANIMATION_DURATION).setInterpolator(AnimUtils.EASE_IN);
650    }
651
652    private final class LayoutIgnoringListener implements View.OnLayoutChangeListener {
653        @Override
654        public void onLayoutChange(View v,
655                int left,
656                int top,
657                int right,
658                int bottom,
659                int oldLeft,
660                int oldTop,
661                int oldRight,
662                int oldBottom) {
663            v.setLeft(oldLeft);
664            v.setRight(oldRight);
665            v.setTop(oldTop);
666            v.setBottom(oldBottom);
667        }
668    }
669}
670