FloatingChildLayout.java revision 3a53c73f04eef94b311bb0469c1d0ca7059c0411
1/*
2 * Copyright (C) 2011 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.contacts.quickcontact;
18
19import com.android.contacts.R;
20import com.android.contacts.test.NeededForReflection;
21import com.android.contacts.util.SchedulingUtils;
22
23import android.animation.Animator;
24import android.animation.AnimatorListenerAdapter;
25import android.animation.ObjectAnimator;
26import android.content.Context;
27import android.content.res.Resources;
28import android.graphics.Rect;
29import android.graphics.drawable.ColorDrawable;
30import android.graphics.drawable.Drawable;
31import android.util.AttributeSet;
32import android.util.Log;
33import android.view.MotionEvent;
34import android.view.View;
35import android.view.animation.AnimationUtils;
36import android.widget.FrameLayout;
37import android.widget.PopupWindow;
38
39/**
40 * Layout containing single child {@link View} which it attempts to center
41 * around {@link #setChildTargetScreen(Rect)}.
42 * <p>
43 * Updates drawable state to be {@link android.R.attr#state_first} when child is
44 * above target, and {@link android.R.attr#state_last} when child is below
45 * target. Also updates {@link Drawable#setLevel(int)} on child
46 * {@link View#getBackground()} to reflect horizontal center of target.
47 * <p>
48 * The reason for this approach is because target {@link Rect} is in screen
49 * coordinates disregarding decor insets; otherwise something like
50 * {@link PopupWindow} might work better.
51 */
52public class FloatingChildLayout extends FrameLayout {
53    private static final String TAG = "FloatingChildLayout";
54    private int mFixedTopPosition;
55    private View mChild;
56    private Rect mTargetScreen = new Rect();
57    private final int mAnimationDuration;
58
59    /** The phase of the background dim. This is one of the values of {@link BackgroundPhase}  */
60    private int mBackgroundPhase = BackgroundPhase.BEFORE;
61
62    private ObjectAnimator mBackgroundAnimator = ObjectAnimator.ofInt(this,
63            "backgroundColorAlpha", 0, DIM_BACKGROUND_ALPHA);
64
65    private interface BackgroundPhase {
66        public static final int BEFORE = 0;
67        public static final int APPEARING_OR_VISIBLE = 1;
68        public static final int DISAPPEARING_OR_GONE = 3;
69    }
70
71    /** The phase of the contents window. This is one of the values of {@link ForegroundPhase}  */
72    private int mForegroundPhase = ForegroundPhase.BEFORE;
73
74    private interface ForegroundPhase {
75        public static final int BEFORE = 0;
76        public static final int APPEARING = 1;
77        public static final int IDLE = 2;
78        public static final int DISAPPEARING = 3;
79        public static final int AFTER = 4;
80    }
81
82    // Black, 50% alpha as per the system default.
83    private static final int DIM_BACKGROUND_ALPHA = 0x7F;
84
85    public FloatingChildLayout(Context context, AttributeSet attrs) {
86        super(context, attrs);
87        final Resources resources = getResources();
88        mFixedTopPosition =
89                resources.getDimensionPixelOffset(R.dimen.quick_contact_top_position);
90        mAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime);
91
92        super.setBackground(new ColorDrawable(0));
93    }
94
95    @Override
96    protected void onFinishInflate() {
97        mChild = findViewById(android.R.id.content);
98        mChild.setDuplicateParentStateEnabled(true);
99
100        // this will be expanded in showChild()
101        mChild.setScaleX(0.5f);
102        mChild.setScaleY(0.5f);
103        mChild.setAlpha(0.0f);
104    }
105
106    public View getChild() {
107        return mChild;
108    }
109
110    /**
111     * FloatingChildLayout manages its own background, don't set it.
112     */
113    @Override
114    public void setBackground(Drawable background) {
115        Log.wtf(TAG, "don't setBackground(), it is managed internally");
116    }
117
118    /**
119     * Set {@link Rect} in screen coordinates that {@link #getChild()} should be
120     * centered around.
121     */
122    public void setChildTargetScreen(Rect targetScreen) {
123        mTargetScreen = targetScreen;
124        requestLayout();
125    }
126
127    /**
128     * Return {@link #mTargetScreen} in local window coordinates, taking any
129     * decor insets into account.
130     */
131    private Rect getTargetInWindow() {
132        final Rect windowScreen = new Rect();
133        getWindowVisibleDisplayFrame(windowScreen);
134
135        final Rect target = new Rect(mTargetScreen);
136        target.offset(-windowScreen.left, -windowScreen.top);
137        return target;
138    }
139
140    @Override
141    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
142
143        final View child = mChild;
144        final Rect target = getTargetInWindow();
145
146        final int childWidth = child.getMeasuredWidth();
147        final int childHeight = child.getMeasuredHeight();
148
149        if (mFixedTopPosition != -1) {
150            // Horizontally centered, vertically fixed position
151            final int childLeft = (getWidth() - childWidth) / 2;
152            final int childTop = mFixedTopPosition;
153            layoutChild(child, childLeft, childTop);
154        } else {
155            // default is centered horizontally around target...
156            final int childLeft = target.centerX() - (childWidth / 2);
157            // ... and vertically aligned a bit below centered
158            final int childTop = target.centerY() - Math.round(childHeight * 0.35f);
159
160            // when child is outside bounds, nudge back inside
161            final int clampedChildLeft = clampDimension(childLeft, childWidth, getWidth());
162            final int clampedChildTop = clampDimension(childTop, childHeight, getHeight());
163
164            layoutChild(child, clampedChildLeft, clampedChildTop);
165        }
166    }
167
168    private static int clampDimension(int value, int size, int max) {
169        // when larger than bounds, just center
170        if (size > max) {
171            return (max - size) / 2;
172        }
173
174        // clamp to bounds
175        return Math.min(Math.max(value, 0), max - size);
176    }
177
178    private static void layoutChild(View child, int left, int top) {
179        child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());
180    }
181
182    @NeededForReflection
183    public void setBackgroundColorAlpha(int alpha) {
184        setBackgroundColor(alpha << 24);
185    }
186
187    public void fadeInBackground() {
188        if (mBackgroundPhase == BackgroundPhase.BEFORE) {
189            mBackgroundPhase = BackgroundPhase.APPEARING_OR_VISIBLE;
190
191            createChildLayer();
192
193            SchedulingUtils.doAfterDraw(this, new Runnable() {
194                @Override
195                public void run() {
196                    mBackgroundAnimator.setDuration(mAnimationDuration).start();
197                }
198            });
199        }
200    }
201
202    public void fadeOutBackground() {
203        if (mBackgroundPhase == BackgroundPhase.APPEARING_OR_VISIBLE) {
204            mBackgroundPhase = BackgroundPhase.DISAPPEARING_OR_GONE;
205            if (mBackgroundAnimator.isRunning()) {
206                mBackgroundAnimator.reverse();
207            } else {
208                ObjectAnimator.ofInt(this, "backgroundColorAlpha", DIM_BACKGROUND_ALPHA, 0).
209                        setDuration(mAnimationDuration).start();
210            }
211        }
212    }
213
214    public boolean isContentFullyVisible() {
215        return mForegroundPhase == ForegroundPhase.IDLE;
216    }
217
218    /** Begin animating {@link #getChild()} visible. */
219    public void showContent(final Runnable onAnimationEndRunnable) {
220        if (mForegroundPhase == ForegroundPhase.BEFORE) {
221            mForegroundPhase = ForegroundPhase.APPEARING;
222            animateScale(false, onAnimationEndRunnable);
223        }
224    }
225
226    /**
227     * Begin animating {@link #getChild()} invisible. Returns false if animation is not valid in
228     * this state
229     */
230    public boolean hideContent(final Runnable onAnimationEndRunnable) {
231        if (mForegroundPhase == ForegroundPhase.APPEARING ||
232                mForegroundPhase == ForegroundPhase.IDLE) {
233            mForegroundPhase = ForegroundPhase.DISAPPEARING;
234
235            createChildLayer();
236
237            animateScale(true, onAnimationEndRunnable);
238            return true;
239        } else {
240            return false;
241        }
242    }
243
244    private void createChildLayer() {
245        mChild.invalidate();
246        mChild.setLayerType(LAYER_TYPE_HARDWARE, null);
247        mChild.buildLayer();
248    }
249
250    /** Creates the open/close animation */
251    private void animateScale(
252            final boolean isExitAnimation,
253            final Runnable onAnimationEndRunnable) {
254        mChild.setPivotX(mTargetScreen.centerX() - mChild.getLeft());
255        mChild.setPivotY(mTargetScreen.centerY() - mChild.getTop());
256
257        final int scaleInterpolator = isExitAnimation
258                ? android.R.interpolator.accelerate_quint
259                : android.R.interpolator.decelerate_quint;
260        final float scaleTarget = isExitAnimation ? 0.5f : 1.0f;
261
262        mChild.animate()
263                .setDuration(mAnimationDuration)
264                .setInterpolator(AnimationUtils.loadInterpolator(getContext(), scaleInterpolator))
265                .scaleX(scaleTarget)
266                .scaleY(scaleTarget)
267                .alpha(isExitAnimation ? 0.0f : 1.0f)
268                .setListener(new AnimatorListenerAdapter() {
269                    @Override
270                    public void onAnimationEnd(Animator animation) {
271                        mChild.setLayerType(LAYER_TYPE_NONE, null);
272                        if (isExitAnimation) {
273                            if (mForegroundPhase == ForegroundPhase.DISAPPEARING) {
274                                mForegroundPhase = ForegroundPhase.AFTER;
275                                if (onAnimationEndRunnable != null) onAnimationEndRunnable.run();
276                            }
277                        } else {
278                            if (mForegroundPhase == ForegroundPhase.APPEARING) {
279                                mForegroundPhase = ForegroundPhase.IDLE;
280                                if (onAnimationEndRunnable != null) onAnimationEndRunnable.run();
281                            }
282                        }
283                    }
284                });
285    }
286
287    private View.OnTouchListener mOutsideTouchListener;
288
289    public void setOnOutsideTouchListener(View.OnTouchListener listener) {
290        mOutsideTouchListener = listener;
291    }
292
293    @Override
294    public boolean onTouchEvent(MotionEvent event) {
295        // at this point, touch wasn't handled by child view; assume outside
296        if (mOutsideTouchListener != null) {
297            return mOutsideTouchListener.onTouch(this, event);
298        }
299        return false;
300    }
301}
302